Private/Get-Win32ToolkitMsixIdentity.ps1
|
function Get-Win32ToolkitMsixIdentity { <# .SYNOPSIS Reads the package identity (Name/Publisher/Version) from an .msix/.appx package OR a .msixbundle/.appxbundle — detected from the file's CONTENT, never its extension. .DESCRIPTION Host-side (PS 7.2+). Opens the package as a ZIP and reads whichever identity manifest it actually carries: * plain package -> ROOT AppxManifest.xml (<Package><Identity .../>) * bundle -> AppxMetadata/AppxBundleManifest.xml (<Bundle><Identity .../>) Both carry the same Identity Name/Publisher, which is the whole point of a bundle: one identity, several architectures. CONTENT, not extension, decides — because the extension lies. winget names Microsoft's PowerShell download '.msix' (its manifest says InstallerType: msix) even though the URL is a .msixbundle. Trusting the extension is exactly what previously made identity extraction return $null for such packages, which left the project with NO Uninstall section at all: the app installed and its uninstall then silently did nothing. A bundle's Identity VERSION is the bundle's own version (often date-stamped) and does NOT track the app version — so it is returned for information only. Nothing keys off it: the device resolves the package at uninstall time by NAME (Get-AppxPackage .Name / Get-AppxProvisionedPackage .DisplayName), which is identical for a bundle and its nested packages, so no PackageFamilyName hash computation is needed. Values are returned as DATA for AppConfig.json — they never enter a code position. .PARAMETER Path Full path to the .msix/.appx/.msixbundle/.appxbundle file. .OUTPUTS [pscustomobject] @{ PackageName; Publisher; Version; IsBundle } — or $null (with a warning) on any failure. #> [CmdletBinding()] [OutputType([pscustomobject])] param( [Parameter(Mandatory)] [ValidateNotNullOrEmpty()] [string]$Path ) if (-not (Test-Path -LiteralPath $Path)) { Write-Warning "MSIX package not found: $Path" return $null } $zip = $null try { $zip = [System.IO.Compression.ZipFile]::OpenRead($Path) # Content detection: a plain package has a ROOT AppxManifest.xml; a bundle has # AppxMetadata/AppxBundleManifest.xml instead. FullName match — a bundle's NESTED per-package # manifests live under a path and can never be mistaken for the root one. $entry = $zip.Entries | Where-Object { $_.FullName -ieq 'AppxManifest.xml' } | Select-Object -First 1 $isBundle = $false if (-not $entry) { $entry = $zip.Entries | Where-Object { $_.FullName -ieq 'AppxMetadata/AppxBundleManifest.xml' } | Select-Object -First 1 if ($entry) { $isBundle = $true } } if (-not $entry) { Write-Warning "No AppxManifest.xml or AppxMetadata/AppxBundleManifest.xml in '$([System.IO.Path]::GetFileName($Path))' — not a readable MSIX/APPX package or bundle." return $null } $reader = New-Object System.IO.StreamReader($entry.Open()) try { [xml]$manifest = $reader.ReadToEnd() } finally { $reader.Dispose() } # Root element differs (<Package> vs <Bundle>) and the two manifests use different XML # namespaces, so select the Identity by LOCAL name among the root's DIRECT CHILDREN. # Deliberately NOT GetElementsByTagName: that is a DESCENDANT search on the QUALIFIED name, so # (a) a nested <Identity> appearing earlier in document order (e.g. inside <Properties>, or a # decoy) would win over the real one, and (b) a namespace-PREFIXED manifest (<b:Identity ...>) # would not match 'Identity' at all and the whole package would read as identity-less. Matching # LocalName on direct children is both namespace-agnostic and immune to nested decoys — and # preserves the old $manifest.Package.Identity child-access semantics. $identity = $manifest.DocumentElement.ChildNodes | Where-Object { $_.NodeType -eq 'Element' -and $_.LocalName -eq 'Identity' } | Select-Object -First 1 # GetAttribute, not property access: on a nameless <Identity>, PowerShell's XML adapter falls # back to the base XmlNode.Name property and '$identity.Name' returns the literal 'Identity' — # which would flow into AppConfig as a bogus PackageName and make the device-side uninstall # report success without removing anything. GetAttribute returns '' when absent. $name = if ($identity -is [System.Xml.XmlElement]) { $identity.GetAttribute('Name') } else { $null } if ([string]::IsNullOrEmpty($name)) { $which = if ($isBundle) { 'AppxBundleManifest.xml' } else { 'AppxManifest.xml' } Write-Warning "$which in '$([System.IO.Path]::GetFileName($Path))' has no Identity Name." return $null } return [pscustomobject]@{ PackageName = $name Publisher = $identity.GetAttribute('Publisher') # For a bundle this is the BUNDLE's version (often date-stamped), not the app's — nothing # keys off it; uninstall resolves by Name. Version = $identity.GetAttribute('Version') IsBundle = $isBundle } } catch { Write-Warning "Could not read MSIX identity from '$Path': $($_.Exception.Message)" return $null } finally { if ($zip) { $zip.Dispose() } } } |