Modules/businessdev.ALbuild.Core/Public/Expand-BcAppFile.ps1
|
function Expand-BcAppFile { <# .SYNOPSIS Extracts a Business Central .app/.runtime.app package and reads its manifest. .DESCRIPTION A BC .app file is a ZIP archive preceded by a small binary runtime header. This function locates the ZIP payload (by its local-file-header signature), extracts it, and parses the embedded NavxManifest.xml to return the app's identity and dependencies - all without a running container. Namespace/casing differences are tolerated by matching element local-names. .PARAMETER Path Path to the .app or .runtime.app file. .PARAMETER DestinationPath Optional folder to extract into. Defaults to a unique temp folder (returned as ExtractedPath); the caller owns cleanup. .EXAMPLE $info = Expand-BcAppFile -Path .\Publisher_App_1.0.0.0.app $info.Id; $info.Application; $info.Dependencies .OUTPUTS PSCustomObject with Id, Name, Publisher, Version, Application, Platform, Dependencies, ManifestPath, ExtractedPath, Manifest (xml). #> [CmdletBinding()] param( [Parameter(Mandatory, Position = 0, ValueFromPipeline, ValueFromPipelineByPropertyName)] [Alias('FullName')] [ValidateNotNullOrEmpty()] [string] $Path, [string] $DestinationPath ) process { if (-not (Test-Path -LiteralPath $Path -PathType Leaf)) { throw "App file not found: '$Path'." } $tempZip = $null try { $bytes = [System.IO.File]::ReadAllBytes((Resolve-Path -LiteralPath $Path).ProviderPath) # Find the ZIP local-file-header signature: 0x50 0x4B 0x03 0x04 ("PK.."). # Use Array.IndexOf to locate candidate 0x50 bytes (native scan) rather than a # per-element PowerShell loop, which is slow over large packages. $zipStart = -1 $from = 0 while (($candidate = [System.Array]::IndexOf($bytes, [byte]0x50, $from)) -ge 0 -and $candidate -le ($bytes.Length - 4)) { if ($bytes[$candidate + 1] -eq 0x4B -and $bytes[$candidate + 2] -eq 0x03 -and $bytes[$candidate + 3] -eq 0x04) { $zipStart = $candidate break } $from = $candidate + 1 } if ($zipStart -lt 0) { throw "'$Path' does not contain a ZIP payload; it may not be a valid .app file." } # Write the ZIP payload from the offset without allocating a second large array. $tempZip = [System.IO.Path]::Combine([System.IO.Path]::GetTempPath(), [System.Guid]::NewGuid().ToString() + '.zip') $zipStream = [System.IO.File]::Open($tempZip, [System.IO.FileMode]::Create, [System.IO.FileAccess]::Write) try { $zipStream.Write($bytes, $zipStart, $bytes.Length - $zipStart) } finally { $zipStream.Dispose() } if (-not $DestinationPath) { $DestinationPath = [System.IO.Path]::Combine([System.IO.Path]::GetTempPath(), 'ALbuild-app-' + [System.Guid]::NewGuid().ToString()) } if (-not (Test-Path -LiteralPath $DestinationPath)) { New-Item -Path $DestinationPath -ItemType Directory -Force | Out-Null } Expand-Archive -LiteralPath $tempZip -DestinationPath $DestinationPath -Force $manifestFile = Get-ChildItem -LiteralPath $DestinationPath -Filter 'NavxManifest.xml' -Recurse -File | Select-Object -First 1 if (-not $manifestFile) { throw "NavxManifest.xml was not found inside '$Path'." } [xml] $manifest = Get-Content -LiteralPath $manifestFile.FullName -Raw -Encoding UTF8 $appNode = $manifest.SelectSingleNode("//*[local-name()='App']") if (-not $appNode) { throw "NavxManifest.xml in '$Path' has no <App> element." } $getAttr = { param($node, [string] $name) if ($null -eq $node -or $null -eq $node.Attributes) { return $null } foreach ($attr in $node.Attributes) { if ($attr.Name -ieq $name) { return $attr.Value } } return $null } $dependencies = @() foreach ($dep in $manifest.SelectNodes("//*[local-name()='Dependency']")) { $minVersion = & $getAttr $dep 'MinVersion' if (-not $minVersion) { $minVersion = & $getAttr $dep 'Version' } $dependencies += [PSCustomObject]@{ Id = & $getAttr $dep 'Id' Name = & $getAttr $dep 'Name' Publisher = & $getAttr $dep 'Publisher' Version = $minVersion } } return [PSCustomObject]@{ Id = & $getAttr $appNode 'Id' Name = & $getAttr $appNode 'Name' Publisher = & $getAttr $appNode 'Publisher' Version = & $getAttr $appNode 'Version' Application = & $getAttr $appNode 'Application' Platform = & $getAttr $appNode 'Platform' Dependencies = $dependencies ManifestPath = $manifestFile.FullName ExtractedPath = $DestinationPath Manifest = $manifest } } finally { if ($tempZip -and (Test-Path -LiteralPath $tempZip)) { Remove-Item -LiteralPath $tempZip -Force -ErrorAction SilentlyContinue } } } } |