Export/Private/Get-D365BCManifestFromAppFile.ps1

function Global:Get-D365BCManifestFromAppFile {
    [CmdletBinding()]
    <#
    .SYNOPSIS
        Load Package-information from App-File
    .DESCRIPTION
        This CmdLet is an alternative to the standard CmdLet Get-NAVAppInfo -Path '.\MyAppFile.app'
        You can use it on a machine, where the standard CmdLet is not available, due to missing DLLs etc.
 
        Reads a binary Extension (.app-File) and returns an [Xml]-object containing the Package-Information.
 
        Use like this:
        $xmlManifest = Get-D365BCManifestFromAppFile -Filename "\\Path\to\my\app.file"
 
        # Possible Properties to check are
        $xmlManifest.App.Id
        $xmlManifest.App.Publisher
        $xmlManifest.App.Name
        $xmlManifest.App.Version
        $xmlManifest.App.Brief
        $xmlManifest.App.Description
        $xmlManifest.App.CompatibilityId
        $xmlManifest.App.PrivacyStatement
        $xmlManifest.App.ApplicationInsightsKey
        $xmlManifest.App.EULA
        $xmlManifest.App.Help
        $xmlManifest.App.HelpBaseUrl
        $xmlManifest.App.ContextSensitiveHelpUrl
        $xmlManifest.App.Url
        $xmlManifest.App.Logo
        $xmlManifest.App.Platform
        $xmlManifest.App.Runtime
        $xmlManifest.App.Target
        $xmlManifest.App.ShowMyCode
    .PARAMETER Filename
        [string] The .app-File you want to grab the information from (mandatory)
    .PARAMETER FullExtract
        [switch] Always extract complete archive, not only NavxManifest.xml
    .PARAMETER SkipCleanup
        [switch] Does not remove extracted files after cleanup
    .PARAMETER HideProgress
        [switch] When full extraction is done, normally there is a ProgressBar indicating the progress of Expand-Archive. Use this switch to hide this ProgressBar
    #>

    param(
        [parameter(Mandatory = $true)]
        [string]
        $Filename,
        [switch]
        $FullExtract,
        [switch]
        $SkipCleanup,
        [switch]
        $HideProgress
    )
    begin {
        Add-Type -Assembly System.IO
        function Remove-InvalidFileNameChars {
            param(
                [Parameter(Mandatory = $true,
                    Position = 0,
                    ValueFromPipeline = $true,
                    ValueFromPipelineByPropertyName = $true)]
                [String]$Name
            )
          
            $invalidChars = [IO.Path]::GetInvalidFileNameChars() -join ''
            $re = "[{0}]" -f [RegEx]::Escape($invalidChars)
            return ($Name -replace $re)
        }
        function Copy-FileToTemporaryLocation {
            param(
                [parameter(Mandatory = $true)]
                [string]
                $Filename
            )
            Write-Verbose "Copying file $Filename to temporary directory"
            $onlyFilename = (Split-Path $Filename -Leaf).Replace(".app", "")
            $onlyFilename = Remove-InvalidFileNameChars $onlyFilename
            $targetTempFolder = Join-Path -Path $env:TEMP -ChildPath (New-Guid).Guid
            $targetTempFolder = Join-Path -Path $targetTempFolder -ChildPath $onlyFilename            
            if (Test-Path $targetTempFolder) {
                Write-Verbose "Removing temporary path $targetTempFolder"
                Remove-Item $targetTempFolder -Force -Recurse
            }
            Write-Verbose "Creating temporary path $targetTempFolder"
            New-Item -Path $targetTempFolder -ItemType Directory -ErrorAction SilentlyContinue | Out-Null
            $NewFilename = Join-Path -Path $targetTempFolder -ChildPath "$onlyFilename.app"
            Write-Verbose "Copying $NewFilename to temporary path $targetTempFolder (as $NewFilename)"
            Copy-Item $Filename -Destination $NewFilename | Out-Null
            $NewFilename
        }
        function Get-NavxManifestFromAppFile {
            param(
                [parameter(Mandatory = $true)]
                [string]
                $Filename,
                [switch]
                $FullExtract,
                [switch]
                $SkipCleanup,
                [switch]
                $HideProgress
            )
            function Switch-AppFileToRegularZipFile {
                param(
                    [parameter(Mandatory = $true)]
                    [string]
                    $Filename
                )
                begin {
                    # Loads an additional Type, called "D365BCAppHelper.StreamHelper", which is used to decode RuntimePackages
                    Add-D365BCDotNetHelperType
                }
                process {
                    Write-Verbose "Creating ZIP-file from APP-file"
                    $onlyFilename = (Split-Path $Filename -Leaf).Replace(".app", "")
                    $parentDirectory = Split-Path -Path $Filename
                    $newFilename = Join-Path -Path $parentDirectory -ChildPath "$onlyFilename.zip"
                    $regularStream = $true;
                    if ([D365BCAppHelper.StreamHelper]::IsRuntimePackage($Filename)) {
                        $regularStream = $false
                    }
                    # App-files are basically ZIP-files, but with an offest of 40 bytes
                    # So we first read the source file into a FileStream
                    # and then set the Offset of the Stream to 40
                    # After that we create a new file, copy the offsetted stream into it and save it
                    $stream = [System.IO.FileStream]::new($Filename, [System.IO.FileMode]::Open)
                    if ($regularStream -eq $true) {
                        $stream.Seek(40, [System.IO.SeekOrigin]::Begin) | Out-Null
                    }
                    else {
                        $newStream = [D365BCAppHelper.StreamHelper]::DecodeStream($stream)
                        $stream.Close()
                        $stream = $newStream
                        $stream.Seek(0, [System.IO.SeekOrigin]::Begin) | Out-Null
                    }
                    $fileStream = [System.IO.File]::Create($newFilename)
                    $stream.CopyTo($fileStream)
                    $fileStream.Close()
                    $stream.Close()
                    Write-Verbose "New ZIP-file is located at $newFilename"
                    return , $newFilename
                }
            }
            function Expand-ArchiveAndReturnManifestName {
                param(
                    [parameter(Mandatory = $true)]
                    [string]
                    $Filename,
                    [switch]
                    $HideProgress
                )
                if ($HideProgress -eq $true) {
                    $ProgressPreferenceBackup = $ProgressPreference
                    $global:ProgressPreference = 'SilentlyContinue'
                }
                $parentDirectory = Split-Path -Path $Filename
                $targetTempFolder = Join-Path -Path $parentDirectory -ChildPath "unzip"
                try {                    
                    Write-Verbose "Extracting $(Split-Path $Filename -Leaf) to $($targetTempFolder)"
                    Expand-Archive -Path $Filename -DestinationPath $targetTempFolder -Force
                }
                catch {
                    Write-Error "An error happened: $_"
                }
                finally {
                    if ($HideProgress -eq $true) {
                        $global:ProgressPreference = $ProgressPreferenceBackup
                    }
                }
                $targetFilename = Join-Path -Path $targetTempFolder -ChildPath "NavxManifest.xml"
                if (-not(Test-Path $targetFilename)) {
                    throw "$targetFilename not found in Archive."
                    return ""
                }
                $targetFilename
            }
            function Get-NavxManifestFileFromArchive {
                param(
                    [parameter(Mandatory = $true)]
                    [string]
                    $Filename
                )
                begin {
                    Add-Type -Assembly System.IO.Compression
                }
                process {
                    $parentDirectory = Split-Path -Path $Filename
                    $targetTempFolder = Join-Path -Path $parentDirectory -ChildPath "unzip"
                    $targetFilename = Join-Path -Path $targetTempFolder -ChildPath "NavxManifest.xml"
                    $targetFilename2 = Join-Path -Path $targetTempFolder -ChildPath "EmittedContent.json"
                    try {                    
                        Write-Verbose "Extracting $(Split-Path $Filename -Leaf) to $($targetTempFolder)"
                        New-Item -Path $targetTempFolder -ItemType Directory -ErrorAction SilentlyContinue | Out-Null
                        
                        # Read file as ZipArchive
                        # Get ZipArchiveEntry for NavxManifest.xml
                        # Save Stream from ZipArchiveEntry as newly created file
                        $zipFileStream = [System.IO.FileStream]::new($Filename, [System.IO.FileMode]::Open)
                        $zipFile = [System.IO.Compression.ZipArchive]::new($zipFileStream, [System.IO.Compression.ZipArchiveMode]::Read)
                        $zipEntryManifest = $zipFile.GetEntry("NavxManifest.xml")
                        $entryStream = $zipEntryManifest.Open()

                        $fileStreamTargetManifest = [System.IO.File]::Create($targetFilename)
                        $entryStream.CopyTo($fileStreamTargetManifest)
                        $entryStream.Close()
                        $zipFileStream.Close()
                        $fileStreamTargetManifest.Close()

                        ### Read "EmittedContent.json" if available
                        $zipFileStream = [System.IO.FileStream]::new($Filename, [System.IO.FileMode]::Open)
                        $zipFile = [System.IO.Compression.ZipArchive]::new($zipFileStream, [System.IO.Compression.ZipArchiveMode]::Read)
                        $zipEntryManifest = $zipFile.GetEntry("bin/EmittedContent.json")
                        if ($zipEntryManifest) {
                            $entryStream = $zipEntryManifest.Open()

                            $fileStreamTargetManifest = [System.IO.File]::Create($targetFilename2)
                            $entryStream.CopyTo($fileStreamTargetManifest)
                            $entryStream.Close()
                            $zipFileStream.Close()
                            $fileStreamTargetManifest.Close()
                        }
                        else {
                            $zipFileStream.Close()
                        }
                    }
                    catch {
                        Write-Error "An error happened: $_"
                    } 
                    if (-not(Test-Path $targetFilename)) {
                        $targetFilename = ""
                    }
                    $targetFilename
                }
            }
            $Filename = Copy-FileToTemporaryLocation -Filename $Filename
            $Filename = Switch-AppFileToRegularZipFile -Filename $Filename
            $ManifestFileName = ""
            if ($FullExtract -ne $true) {
                # For performance reasons, first try to read a single entry from the archive
                # if this fails, extract the complete archive and look for the file
                $ManifestFileName = Get-NavxManifestFileFromArchive -Filename $Filename
            }
            if ([string]::IsNullOrEmpty($ManifestFileName)) {
                # Fallback (Extract complete Archive)
                $ManifestFileName = Expand-ArchiveAndReturnManifestName -Filename $Filename -HideProgress:$HideProgress
            }
            if ([string]::IsNullOrEmpty($ManifestFileName)) {
                return
            }
            $parentPath = Split-Path -Path $ManifestFileName -Parent
            $additionalContentPath = Join-Path $parentPath -ChildPath "EmittedContent.json"            

            [Xml]$xmlManifest = Get-Content -Path $ManifestFileName -Encoding UTF8
            if (Test-Path -Path $additionalContentPath) {
                $additionalContent = Get-Content -Raw -Path $additionalContentPath | ConvertFrom-Json
                if ($additionalContent.PlatformVersion) {
                    $child = $xmlManifest.CreateElement("PlatformVersion")
                    $child.InnerText = $("$($additionalContent.PlatformVersion.Major).$($additionalContent.PlatformVersion.Minor).$($additionalContent.PlatformVersion.Build).$($additionalContent.PlatformVersion.Revision)")
                    $xmlManifest.Package.App.AppendChild($child)
                }
            }
            if ($SkipCleanup -eq $false) {
                # see https://github.com/SimonOfHH/D365BCAppHelper/issues/10
                # It seems that there is a problem in some constellations that, if the directory name would end
                # with a dot (.) the "Remove-Item" will throw an error. So, if the directory name would end with a
                # dot remove it here
                $cleanUpPath = Split-Path $Filename
                if ($cleanUpPath.EndsWith(".")) {
                    $cleanUpPath = $cleanUpPath.Substring(0, $cleanUpPath.Length - 1)
                }
                Write-Verbose "Cleaning up / removing temporary path $($cleanUpPath)"
                Remove-Item $cleanUpPath -Force -Recurse
            }
            $xmlManifest.Package
        }
    }
    process {
        if (-not(Test-Path $Filename)) {
            throw "$Filename does not exist."
        }
        Write-Verbose "Getting information from $Filename"
        Get-NavxManifestFromAppFile -Filename $Filename -FullExtract:$FullExtract -SkipCleanup:$SkipCleanup -HideProgress:$HideProgress
    }
}
Export-ModuleMember Get-D365BCManifestFromAppFile