Public/Test-MSIXManifest.ps1
|
function Test-MSIXManifest { <# .SYNOPSIS Validates an AppxManifest.xml against the schemas embedded in AppxPackaging.dll. .DESCRIPTION Loads all XSD schemas directly from AppxPackaging.dll (no disk export needed), builds an in-memory XmlSchemaSet, and validates the specified manifest file. Validation errors and warnings are written via Write-Warning. Returns $true if the manifest is valid, $false otherwise. .PARAMETER ManifestPath Path to the AppxManifest.xml file to validate. .PARAMETER DllPath Path to AppxPackaging.dll. Defaults to C:\Windows\System32\AppxPackaging.dll. .EXAMPLE Test-MSIXManifest -ManifestPath "C:\MSIXTemp\WinRAR\AppxManifest.xml" .EXAMPLE if (-not (Test-MSIXManifest -ManifestPath $manifest -Verbose)) { throw "Manifest validation failed." } .NOTES https://www.nick-it.de Andreas Nick, 2026 #> [CmdletBinding()] [OutputType([bool])] param( [Parameter(Mandatory = $true, Position = 0)] [string] $ManifestPath, [string] $DllPath = 'C:\Windows\System32\AppxPackaging.dll' ) # P/Invoke helpers to enumerate and read Win32 resources from the DLL if (-not ([System.Management.Automation.PSTypeName]'ResEx').Type) { $code = @' using System; using System.Runtime.InteropServices; using System.Collections.Generic; public class ResEx { [DllImport("kernel32.dll", CharSet=CharSet.Unicode)] public static extern IntPtr LoadLibraryEx(string f, IntPtr h, uint flags); [DllImport("kernel32.dll")] public static extern bool FreeLibrary(IntPtr h); [DllImport("kernel32.dll", CharSet=CharSet.Unicode)] public static extern IntPtr FindResource(IntPtr h, IntPtr name, IntPtr type); [DllImport("kernel32.dll")] public static extern IntPtr LoadResource(IntPtr h, IntPtr r); [DllImport("kernel32.dll")] public static extern IntPtr LockResource(IntPtr r); [DllImport("kernel32.dll")] public static extern uint SizeofResource(IntPtr h, IntPtr r); public delegate bool EnumTypesProc(IntPtr h, IntPtr type, IntPtr p); public delegate bool EnumNamesProc(IntPtr h, IntPtr type, IntPtr name, IntPtr p); [DllImport("kernel32.dll", CharSet=CharSet.Unicode)] public static extern bool EnumResourceTypes(IntPtr h, EnumTypesProc proc, IntPtr p); [DllImport("kernel32.dll", CharSet=CharSet.Unicode)] public static extern bool EnumResourceNames(IntPtr h, IntPtr type, EnumNamesProc proc, IntPtr p); public static List<long> Types = new List<long>(); public static List<Tuple<long,long>> Names = new List<Tuple<long,long>>(); public static void CollectTypes(IntPtr h) { Types.Clear(); EnumResourceTypes(h, (hh, type, p) => { Types.Add(type.ToInt64()); return true; }, IntPtr.Zero); } public static void CollectNames(IntPtr h, IntPtr type) { EnumResourceNames(h, type, (hh, t, name, p) => { Names.Add(Tuple.Create(type.ToInt64(), name.ToInt64())); return true; }, IntPtr.Zero); } public static byte[] GetData(IntPtr h, IntPtr name, IntPtr type) { IntPtr r = FindResource(h, name, type); if (r == IntPtr.Zero) return null; uint sz = SizeofResource(h, r); IntPtr data = LockResource(LoadResource(h, r)); byte[] buf = new byte[sz]; Marshal.Copy(data, buf, 0, (int)sz); return buf; } } '@ Add-Type -TypeDefinition $code } $DllPath = [System.IO.Path]::GetFullPath($DllPath) $ManifestPath = [System.IO.Path]::GetFullPath($ManifestPath) if (-not (Test-Path $DllPath)) { Write-Error "AppxPackaging.dll not found: $DllPath" return $false } if (-not (Test-Path $ManifestPath)) { Write-Error "Manifest not found: $ManifestPath" return $false } $hMod = [ResEx]::LoadLibraryEx($DllPath, [IntPtr]::Zero, 0x00000002) if ($hMod -eq [IntPtr]::Zero) { Write-Error "Could not load DLL: $DllPath" return $false } # Force English so System.Xml.Schema messages are not localized to the OS UI language. $savedCulture = [System.Threading.Thread]::CurrentThread.CurrentUICulture [System.Threading.Thread]::CurrentThread.CurrentUICulture = New-Object System.Globalization.CultureInfo('en-US') try { # Enumerate all resources and extract XSD data indexed by targetNamespace [ResEx]::CollectTypes($hMod) [ResEx]::Names.Clear() foreach ($typeId in [ResEx]::Types) { [ResEx]::CollectNames($hMod, [IntPtr]$typeId) } $schemaBytes = @{} foreach ($entry in [ResEx]::Names) { $data = [ResEx]::GetData($hMod, [IntPtr]$entry.Item2, [IntPtr]$entry.Item1) if ($null -eq $data -or $data.Length -lt 10) { continue } $preview = [System.Text.Encoding]::UTF8.GetString($data, 0, [Math]::Min(200, $data.Length)) $trimmed = $preview.TrimStart() if ($trimmed -notlike '<xs:*' -and $trimmed -notlike '<?xml*') { continue } try { $xDoc = New-Object xml $xDoc.LoadXml([System.Text.Encoding]::UTF8.GetString($data)) $ns = $xDoc.DocumentElement.GetAttribute('targetNamespace') if ($ns -and -not $schemaBytes.ContainsKey($ns)) { $schemaBytes[$ns] = $data } } catch { } } Write-Verbose "Loaded $($schemaBytes.Count) schemas from AppxPackaging.dll." # Build XmlSchemaSet — add all schemas before compiling so cross-namespace # imports resolve by namespace URI rather than schemaLocation hint. $schemaSet = New-Object System.Xml.Schema.XmlSchemaSet # Suppress schema compilation warnings/errors — they are xs:import resolution artefacts # from in-memory loading and do not reflect manifest validity. $schemaSet.add_ValidationEventHandler( [System.Xml.Schema.ValidationEventHandler] { } ) foreach ($ns in $schemaBytes.Keys) { $stream = New-Object System.IO.MemoryStream($schemaBytes[$ns], $false) $reader = [System.Xml.XmlReader]::Create($stream) try { $null = $schemaSet.Add($ns, $reader) } catch { Write-Verbose "Could not add schema for namespace '$ns': $_" } finally { $reader.Dispose() $stream.Dispose() } } try { $schemaSet.Compile() } catch { Write-Verbose "Schema set compile error (non-fatal): $_" } # Schema compilation issues (unresolvable xs:import schemaLocation hints when loading # in-memory) are intentionally suppressed — they do not affect manifest validation. # Validate the manifest $validationErrors = New-Object System.Collections.Generic.List[string] $settings = New-Object System.Xml.XmlReaderSettings $settings.ValidationType = [System.Xml.ValidationType]::Schema $settings.Schemas = $schemaSet $settings.ValidationFlags = ` [System.Xml.Schema.XmlSchemaValidationFlags]::ProcessIdentityConstraints -bor ` [System.Xml.Schema.XmlSchemaValidationFlags]::ReportValidationWarnings $validationHandler = [System.Xml.Schema.ValidationEventHandler] { param($sender, $e) # "Could not find schema information" warnings are emitted for every element/attribute # when the base schema fails to compile due to unresolvable xs:import schemaLocation # hints (in-memory loading cannot follow file-relative URIs). They are noise, not # real manifest errors. if ($e.Message -like '*Could not find schema information*') { return } $validationErrors.Add("[$($e.Severity)] Line $($e.Exception.LineNumber), Col $($e.Exception.LinePosition): $($e.Message)") } $settings.add_ValidationEventHandler($validationHandler) $manifestReader = [System.Xml.XmlReader]::Create($ManifestPath, $settings) try { while ($manifestReader.Read()) { } } finally { $manifestReader.Dispose() } if ($validationErrors.Count -eq 0) { Write-Verbose "Manifest validation passed: $ManifestPath" return $true } else { foreach ($err in $validationErrors) { Write-Warning $err } return $false } } finally { [System.Threading.Thread]::CurrentThread.CurrentUICulture = $savedCulture [ResEx]::FreeLibrary($hMod) | Out-Null } } |