Public/Sync-OSDAppMicrosoftCompanyPortal.ps1

function Sync-OSDAppMicrosoftCompanyPortal {
    [CmdletBinding(SupportsShouldProcess)]
    param(
        [string]$KitPath,
        [ValidateSet('x64','arm64')][string]$Architecture = 'x64'
    )

    if (Test-OSDAppWinPE) {
        throw 'Prepare the Company Portal cache on full Windows, not in WinPE.'
    }
    $cachePath = Get-OSDAppCachePath
    $target = Join-Path $cachePath ('BuiltIn\MicrosoftCompanyPortal\' + $Architecture)
    if (-not $PSCmdlet.ShouldProcess($target, 'Prepare Microsoft Company Portal from the supplied Preinstall Kit')) { return }

    $automatic = [string]::IsNullOrWhiteSpace($KitPath)
    $downloadUri = 'https://download.microsoft.com/download/ac93b367-7b17-4838-a079-c6f3377bf582/CompanyPortal-Universal-Production_x64_x86_ARM_ARM64.appxupload_Windows10_PreinstallKit.zip'
    $kit = $null
    Add-Type -AssemblyName System.IO.Compression -ErrorAction Stop
    Add-Type -AssemblyName System.IO.Compression.FileSystem -ErrorAction Stop
    $lock = Enter-OSDAppCacheLock -CachePath $cachePath -Operation 'Microsoft Company Portal cache synchronization'
    $candidate = $target + '.osdapps-new-' + [guid]::NewGuid().ToString('N')
    $backup = $target + '.osdapps-old-' + [guid]::NewGuid().ToString('N')
    try {
        if ($automatic) {
            $kit = Join-Path $cachePath ('CompanyPortal-PreinstallKit-' + [guid]::NewGuid().ToString('N') + '.zip')
            Invoke-WebRequest -Uri $downloadUri -OutFile $kit -UseBasicParsing -TimeoutSec 900 -ErrorAction Stop
        } else {
            $kit = (Resolve-Path -LiteralPath $KitPath -ErrorAction Stop).Path
        }
        if ([IO.Path]::GetExtension($kit) -ne '.zip') { throw 'Expected the official Microsoft Preinstall Kit ZIP.' }
        $zip = [IO.Compression.ZipFile]::OpenRead($kit)
        try {
            # Only select vendor files at the ZIP root. Reject ambiguous versions.
            $entries = @($zip.Entries | Where-Object { $_.Name -eq $_.FullName -and $_.Length -gt 0 })
            $bundles = @($entries | Where-Object { $_.Name -match '^[a-zA-Z0-9_-]+\.appxbundle$' })
            $licenses = @($entries | Where-Object { $_.Name -match '^[a-zA-Z0-9_-]+_License[0-9]+\.xml$' })
            if ($bundles.Count -ne 1 -or $licenses.Count -ne 1) {
                throw 'The Preinstall Kit must contain exactly one root appxbundle and one offline license.'
            }
            $families = @(
                'Microsoft.NET.Native.Framework.2.2_',
                'Microsoft.NET.Native.Runtime.2.2_',
                'Microsoft.Services.Store.Engagement_',
                'Microsoft.UI.Xaml.2.7_',
                'Microsoft.VCLibs.140.00_'
            )
            $dependencies = @()
            foreach ($family in $families) {
                $matches = @($entries | Where-Object { $_.Name -like ($family + '*_' + $Architecture + '__*.appx') })
                if ($matches.Count -ne 1) {
                    throw ('Expected one {0} dependency for {1}; found {2}.' -f $family,$Architecture,$matches.Count)
                }
                $dependencies += $matches[0]
            }
            New-Item -ItemType Directory -Path $candidate -Force -ErrorAction Stop | Out-Null
            New-Item -ItemType Directory -Path (Join-Path $candidate 'Dependencies') -Force -ErrorAction Stop | Out-Null

            $selected = @(
                @{ Entry=$bundles[0]; Relative='Bundle.appxbundle' },
                @{ Entry=$licenses[0]; Relative='License.xml' }
            )
            foreach ($dep in $dependencies) {
                $selected += @{ Entry=$dep; Relative=('Dependencies\' + $dep.Name) }
            }
            $files = @()
            foreach ($entry in $selected) {
                $destination = Join-Path $candidate $entry.Relative
                [IO.Compression.ZipFileExtensions]::ExtractToFile($entry.Entry, $destination, $true)
                $files += [pscustomobject]@{
                    Path = [string]$entry.Relative
                    Sha256 = (Get-FileHash -LiteralPath $destination -Algorithm SHA256).Hash
                }
            }
        }
        finally { $zip.Dispose() }

        # Read app identity and version from the actual vendor-signed bundle metadata.
        $bundle = [IO.Compression.ZipFile]::OpenRead((Join-Path $candidate 'Bundle.appxbundle'))
        try {
            $manifestEntry = $bundle.GetEntry('AppxMetadata/AppxBundleManifest.xml')
            if (-not $manifestEntry) { throw 'AppxBundleManifest.xml was not found.' }
            $reader = [IO.StreamReader]::new($manifestEntry.Open())
            try { [xml]$bundleXml = $reader.ReadToEnd() }
            finally { $reader.Dispose() }
            $identity = $bundleXml.SelectSingleNode("//*[local-name()='Identity']")
            if (-not $identity -or $identity.GetAttribute('Name') -ne 'Microsoft.CompanyPortal' -or
                $identity.GetAttribute('Publisher') -notmatch 'Microsoft Corporation') {
                throw 'Unexpected Microsoft Company Portal bundle identity or publisher.'
            }
            $version = $identity.GetAttribute('Version')
            if ($version -notmatch '^\d+\.\d+\.\d+\.\d+$') { throw 'Invalid Company Portal version.' }
        }
        finally { $bundle.Dispose() }

        [ordered]@{
            Id='MicrosoftCompanyPortal'; Version=$version; Architecture=$Architecture
            Source=$(if($automatic){'Microsoft Download Center'}else{'Microsoft Preinstall Kit (operator supplied)'})
            SourceUri=$(if($automatic){$downloadUri}else{$null})
            SourceKitSha256=(Get-FileHash -LiteralPath $kit -Algorithm SHA256).Hash
            Files=@($files); DependencyCount=$dependencies.Count
            SyncedAt=(Get-Date).ToUniversalTime().ToString('o')
        } | ConvertTo-Json -Depth 10 |
            Set-Content -LiteralPath (Join-Path $candidate 'CacheInfo.json') -Encoding UTF8 -ErrorAction Stop

        if (Test-Path -LiteralPath $target) {
            Move-Item -LiteralPath $target -Destination $backup -ErrorAction Stop
        }
        try { Move-Item -LiteralPath $candidate -Destination $target -ErrorAction Stop }
        catch {
            if (Test-Path -LiteralPath $backup) { Move-Item -LiteralPath $backup -Destination $target -ErrorAction SilentlyContinue }
            throw
        }
        if (Test-Path -LiteralPath $backup) { Remove-Item -LiteralPath $backup -Recurse -Force -ErrorAction Stop }

        [pscustomobject]@{
            Id='MicrosoftCompanyPortal'; Version=$version; Architecture=$Architecture
            Cached=$true; Updated=$true; CacheReused=$false; CachePath=$target; Files=$files.Count
        }
    }
    catch {
        $originalError = $_
        if ($automatic -and (Test-Path -LiteralPath (Join-Path $target 'CacheInfo.json') -PathType Leaf)) {
            try {
                $previous = Get-Content (Join-Path $target 'CacheInfo.json') -Raw | ConvertFrom-Json
                $healthy = ($previous.Id -eq 'MicrosoftCompanyPortal' -and $previous.Architecture -eq $Architecture -and @($previous.Files).Count -eq 7)
                foreach ($item in @($previous.Files)) {
                    $relative = [string]$item.Path
                    if ($relative -notmatch '^(Bundle\.appxbundle|License\.xml|Dependencies\\[A-Za-z0-9._-]+\.appx)$' -or
                        [string]$item.Sha256 -notmatch '^[0-9A-Fa-f]{64}$') { $healthy = $false; break }
                    $file = Join-Path $target $relative
                    if (-not (Test-Path -LiteralPath $file -PathType Leaf) -or
                        (Get-FileHash -LiteralPath $file -Algorithm SHA256).Hash -ne $item.Sha256) { $healthy = $false; break }
                }
                if ($healthy) {
                    Write-Warning ('Company Portal download failed; verified cache reused: {0}' -f $originalError.Exception.Message)
                    return [pscustomobject]@{Id='MicrosoftCompanyPortal';Version=$previous.Version;Architecture=$Architecture;Cached=$true;Updated=$false;CacheReused=$true;CachePath=$target;Files=7}
                }
            } catch { }
        }
        throw $originalError
    }
    finally {
        if ($automatic -and $kit -and (Test-Path -LiteralPath $kit -PathType Leaf)) {
            Remove-Item -LiteralPath $kit -Force -ErrorAction SilentlyContinue
        }
        if (Test-Path -LiteralPath $candidate) { Remove-Item -LiteralPath $candidate -Recurse -Force -ErrorAction SilentlyContinue }
        if ($lock) { $lock.Dispose() }
    }
}