Get-ReparsePointAppInfo.psm1

<#PSScriptInfo
.SYNOPSIS
    Extracts and decodes information from App Execution Aliases reparse points.
 
.VERSION 1.0
 
.GUID 1ba0d4e4-a6d8-4cda-8372-ee2de24302fd
 
.AUTHOR asklar
 
.COMPANYNAME
 
.COPYRIGHT
 
.TAGS
 
.LICENSEURI
 
.PROJECTURI
 
.ICONURI
 
.EXTERNALMODULEDEPENDENCIES
 
.REQUIREDSCRIPTS
 
.EXTERNALSCRIPTDEPENDENCIES
 
.RELEASENOTES
 
.PRIVATEDATA
 
 
.DESCRIPTION
    This script parses MSIX app execution aliases and extracts application information including package family name, AUMID, and executable path.
 
.PARAMETER Path
    Path to the reparse point file to analyze.
 
.EXAMPLE
    Get-ReparsePointAppInfo -Path "C:\Users\Foo\Bar.exe"
 
.NOTES
    Author: Alexander Sklar
    Version: 1.0
    Date: May 7, 2025
    Requires: Windows PowerShell 5.1 or later
#>


function Get-ReparsePointAppInfo {
    param (
        [Parameter(Mandatory = $true)]
        [string]$Path
    )

    # Run the fsutil command and capture output
    $output = & fsutil reparsepoint query $Path

    # Initialize variables
    $tagValue = ""
    $tagName = ""
    $dataLength = ""
    $hexBytes = New-Object System.Collections.Generic.List[byte]
    $currentSection = ""

    # Process the output line by line
    foreach ($line in $output) {
        if ($line -match "Reparse Tag Value\s*:\s*(.*)") {
            $tagValue = $matches[1].Trim()
        }
        elseif ($line -match "Tag value:\s*(.*)") {
            $tagName = $matches[1].Trim()
        }
        elseif ($line -match "Reparse Data Length:\s*(.*)") {
            $dataLength = $matches[1].Trim()
        }
        elseif ($line -match "Reparse Data:") {
            $currentSection = "reparseData"
        }
        elseif ($currentSection -eq "reparseData" -and $line -match "^\s*[0-9a-fA-F]+:\s+(.*)") {
            # Extract the hex data, ignoring the offset at the beginning
            $hexData = $matches[1].Trim()
            
            # Split by spaces to get individual hex values
            $values = $hexData -split "\s+"
            
            # Convert each hex value to a byte and add to our list
            foreach ($value in $values) {
                if ($value -match "^[0-9a-fA-F]{2}$") {
                    $hexBytes.Add([Convert]::ToByte($value, 16))
                }
            }
        }
    }

    # Extract the 4-byte version value from the beginning
    $version = 0
    if ($hexBytes.Count -ge 4) {
        $version = [BitConverter]::ToInt32($hexBytes.ToArray(), 0)
    }
    
    # Check if the version indicates multi-app support (value 3)
    $hasMultiAppSupport = ($version -eq 3)

    # Process the byte array to extract UTF-16 strings
    $decodedStrings = @()
    $currentBytes = New-Object System.Collections.Generic.List[byte]
    
    # Start after the 4-byte version (index 4)
    for ($i = 4; $i -lt $hexBytes.Count; $i += 2) {
        if ($i + 1 -ge $hexBytes.Count) {
            break
        }
        
        # Get the current two bytes (UTF-16 character)
        $byte1 = $hexBytes[$i]
        $byte2 = $hexBytes[$i + 1]
        
        # If we have a null character (0x0000), end the current string
        if ($byte1 -eq 0 -and $byte2 -eq 0) {
            if ($currentBytes.Count -gt 0) {
                $stringBytes = $currentBytes.ToArray()
                $string = [System.Text.Encoding]::Unicode.GetString($stringBytes)
                if ($string.Length -gt 0) {
                    $decodedStrings += $string
                }
                $currentBytes.Clear()
            }
        } else {
            # Add bytes to the current string
            $currentBytes.Add($byte1)
            $currentBytes.Add($byte2)
        }
    }
    
    # Add the last string if there are remaining bytes
    if ($currentBytes.Count -gt 0) {
        $stringBytes = $currentBytes.ToArray()
        $string = [System.Text.Encoding]::Unicode.GetString($stringBytes)
        if ($string.Length -gt 0) {
            $decodedStrings += $string
        }
    }

    # Map decoded strings to specific categories
    $packageFamilyName = ""
    $aumid = ""
    $executablePath = ""
    $appType = ""

    # Parse the decoded strings based on the corrected mapping
    if ($decodedStrings.Count -ge 1) {
        $packageFamilyName = $decodedStrings[0]  # First string is the package family name
    }
    if ($decodedStrings.Count -ge 2) {
        $aumid = $decodedStrings[1]  # Second string is the AUMID
    }
    if ($decodedStrings.Count -ge 3) {
        $executablePath = $decodedStrings[2]  # Third string is the executable path
    }
    if ($decodedStrings.Count -ge 4) {
        $appType = $decodedStrings[3]  # Fourth string is the app type
    }

    # Return the object with the parsed information
    return [PSCustomObject]@{
        Path = $Path
        ReparseTagValue = $tagValue
        TagName = $tagName
        DataLength = $dataLength
        Version = $version
        MultiAppSupport = $hasMultiAppSupport
        PackageFamilyName = $packageFamilyName
        AUMID = $aumid
        ExecutablePath = $executablePath
        AppType = $appType
    }
}

Export-ModuleMember -Function Get-ReparsePointAppInfo