#Requires -Version 5.1

[CmdletBinding()]
param(
    [Parameter(Mandatory = $true)]
    [string] $ZipPath,

    [Parameter(Mandatory = $true)]
    [string] $ExpectedSha256,

    [Parameter()]
    [string] $RepositoryRoot
)

Set-StrictMode -Version Latest
$ErrorActionPreference = 'Stop'

$manifestFileName = 'LegacyRuntimeBundle.manifest.json'
$expectedResAssetCount = 34
$maximumEntryCount = 10000
$maximumManifestLength = 4MB
$maximumFileLength = 1GB
$maximumPayloadLength = 2GB

function Get-NormalizedFullPath {
    param(
        [Parameter(Mandatory = $true)]
        [string] $Path
    )

    return [IO.Path]::GetFullPath($Path)
}

function Test-PathIsWithin {
    param(
        [Parameter(Mandatory = $true)]
        [string] $Root,

        [Parameter(Mandatory = $true)]
        [string] $Candidate
    )

    $normalizedRoot = (Get-NormalizedFullPath -Path $Root).TrimEnd(
        [IO.Path]::DirectorySeparatorChar,
        [IO.Path]::AltDirectorySeparatorChar)
    $normalizedCandidate = Get-NormalizedFullPath -Path $Candidate
    $prefix = $normalizedRoot + [IO.Path]::DirectorySeparatorChar
    return $normalizedCandidate.StartsWith(
        $prefix,
        [StringComparison]::OrdinalIgnoreCase)
}

function Assert-NoReparsePointInExistingAncestry {
    param(
        [Parameter(Mandatory = $true)]
        [string] $Path,

        [Parameter(Mandatory = $true)]
        [string] $Description
    )

    $current = Get-NormalizedFullPath -Path $Path
    while (-not [string]::IsNullOrEmpty($current)) {
        if (Test-Path -LiteralPath $current) {
            $attributes = [IO.File]::GetAttributes($current)
            if (($attributes -band [IO.FileAttributes]::ReparsePoint) -ne 0) {
                throw "$Description traverses a reparse point: $current"
            }
        }

        $trimmed = $current.TrimEnd(
            [IO.Path]::DirectorySeparatorChar,
            [IO.Path]::AltDirectorySeparatorChar)
        $parent = [IO.Path]::GetDirectoryName($trimmed)
        if ([string]::IsNullOrEmpty($parent) -or
            $parent.Equals($current, [StringComparison]::OrdinalIgnoreCase)) {
            break
        }

        $current = $parent
    }
}

function Assert-NoReparsePointInTree {
    param(
        [Parameter(Mandatory = $true)]
        [string] $Root,

        [Parameter(Mandatory = $true)]
        [string] $Description
    )

    Assert-NoReparsePointInExistingAncestry -Path $Root -Description $Description
    $pending = New-Object 'System.Collections.Generic.Queue[string]'
    $pending.Enqueue((Get-NormalizedFullPath -Path $Root))
    while ($pending.Count -gt 0) {
        $directory = $pending.Dequeue()
        foreach ($child in @(Get-ChildItem -LiteralPath $directory -Force)) {
            if (($child.Attributes -band [IO.FileAttributes]::ReparsePoint) -ne 0) {
                throw "$Description contains a reparse point: $($child.FullName)"
            }
            if ($child.PSIsContainer) {
                $pending.Enqueue($child.FullName)
            }
        }
    }
}

function Assert-SafePayloadRelativePath {
    param(
        [Parameter(Mandatory = $true)]
        [string] $RelativePath
    )

    $portablePath = $RelativePath.Replace('\', '/')
    $segments = @($portablePath.Split('/'))
    $unsafeSegments = @($segments | Where-Object {
        [string]::IsNullOrWhiteSpace($_) -or $_ -eq '.' -or $_ -eq '..'
    })
    if ($segments.Count -lt 2 -or $unsafeSegments.Count -gt 0) {
        throw "Unsafe runtime payload path: $RelativePath"
    }

    foreach ($segment in $segments) {
        if ($segment -match '(?i)^(backup|backups|database|databases|license|licenses|cert|certs|credential|credentials|secret|secrets|vendor)$') {
            throw "Forbidden runtime payload directory or file name: $RelativePath"
        }
    }

    $leafName = $segments[$segments.Count - 1]
    if (-not $leafName.Equals('MmoneyCoder.ico', [StringComparison]::OrdinalIgnoreCase) -and
        $leafName -match '(?i)^MmoneyCoder(?:$|[ ._-])') {
        throw "Credential-bearing or archived MmoneyCoder material is forbidden: $RelativePath"
    }

    if ($leafName.Equals('afiedt.buf.txt', [StringComparison]::OrdinalIgnoreCase) -or
        $leafName -match '(?i)(?:\.bak|\.backup|\.old|\.orig|\.save|\.tmp|\.temp|~)$' -or
        $leafName -match '(?i)(?:^|[ ._-])(backup|copy|\xBCF5\xC0AC\xBCF8)(?:[ ._-]|$)') {
        throw "Backup or temporary material is forbidden: $RelativePath"
    }

    $extension = [IO.Path]::GetExtension($leafName)
    $forbiddenExtensions = @(
        '.dll', '.ocx', '.tlb',
        '.lic', '.license',
        '.pfx', '.p12', '.snk', '.cer', '.crt', '.key', '.pem', '.p7b', '.p7c',
        '.db', '.sqlite', '.sqlite3', '.mdb', '.accdb', '.mdf', '.ldf', '.sql',
        '.zip', '.7z', '.rar',
        '.exe', '.com', '.msi'
    )
    if ($forbiddenExtensions -icontains $extension) {
        throw "Forbidden database, vendor, license, certificate, archive, or executable file: $RelativePath"
    }

    if ($leafName -match '(?i)^(database|credentials?|secrets?|connectionstrings?)(?:[._-]|$)') {
        throw "Credential or database configuration is forbidden: $RelativePath"
    }
}

function Assert-SafeArchiveEntryPath {
    param(
        [Parameter(Mandatory = $true)]
        [string] $EntryPath,

        [Parameter(Mandatory = $true)]
        [bool] $IsDirectory
    )

    if ([string]::IsNullOrWhiteSpace($EntryPath) -or
        $EntryPath.IndexOf([char] 0) -ge 0 -or
        $EntryPath.Contains('\') -or
        $EntryPath.StartsWith('/', [StringComparison]::Ordinal) -or
        $EntryPath -match '^[A-Za-z]:' -or
        $EntryPath.Contains(':')) {
        throw "Unsafe archive entry path: $EntryPath"
    }
    if (-not $EntryPath.Equals(
            $EntryPath.Normalize([Text.NormalizationForm]::FormC),
            [StringComparison]::Ordinal)) {
        throw "Archive entry path must use Unicode NFC normalization: $EntryPath"
    }

    if ($IsDirectory) {
        if (-not $EntryPath.EndsWith('/', [StringComparison]::Ordinal)) {
            throw "Archive directory entry is not canonical: $EntryPath"
        }
        $candidate = $EntryPath.Substring(0, $EntryPath.Length - 1)
    }
    else {
        if ($EntryPath.EndsWith('/', [StringComparison]::Ordinal)) {
            throw "Archive file entry is not canonical: $EntryPath"
        }
        $candidate = $EntryPath
    }

    $segments = @($candidate.Split('/'))
    if ($segments.Count -eq 0) {
        throw "Unsafe archive entry path: $EntryPath"
    }

    $invalidFileNameCharacters = [IO.Path]::GetInvalidFileNameChars()
    foreach ($segment in $segments) {
        if ([string]::IsNullOrWhiteSpace($segment) -or
            $segment -eq '.' -or
            $segment -eq '..' -or
            $segment.EndsWith(' ', [StringComparison]::Ordinal) -or
            $segment.EndsWith('.', [StringComparison]::Ordinal) -or
            $segment.IndexOfAny($invalidFileNameCharacters) -ge 0) {
            throw "Unsafe archive entry path: $EntryPath"
        }

        $deviceStem = $segment.Split('.')[0]
        if ($deviceStem -match '(?i)^(CON|PRN|AUX|NUL|COM[1-9]|LPT[1-9])$') {
            throw "Archive entry uses a reserved Windows file name: $EntryPath"
        }
    }

    return $candidate
}

function Test-ArchiveEntryIsReparsePoint {
    param(
        [Parameter(Mandatory = $true)]
        [IO.Compression.ZipArchiveEntry] $Entry
    )

    $externalAttributes = [BitConverter]::ToUInt32(
        [BitConverter]::GetBytes([int] $Entry.ExternalAttributes),
        0)
    $dosAttributes = $externalAttributes -band [uint32] 0x0000FFFF
    $unixFileType = ($externalAttributes -shr 16) -band [uint32] 0x0000F000
    return (($dosAttributes -band [uint32] [IO.FileAttributes]::ReparsePoint) -ne 0 -or
        $unixFileType -eq [uint32] 0x0000A000)
}

function Get-LegacyPackagedResAssetNames {
    param(
        [Parameter(Mandatory = $true)]
        [string] $ProjectPath
    )

    if (-not (Test-Path -LiteralPath $ProjectPath -PathType Leaf)) {
        throw "Legacy parity project was not found: $ProjectPath"
    }

    [xml] $projectXml = Get-Content -LiteralPath $ProjectPath -Raw -Encoding UTF8
    $nodes = @($projectXml.SelectNodes(
        "/*[local-name()='Project']/*[local-name()='ItemGroup']/*[local-name()='LegacyPackagedResAsset']"))
    if ($nodes.Count -ne $expectedResAssetCount) {
        throw ("The project must declare exactly {0} LegacyPackagedResAsset items; found {1}." -f
            $expectedResAssetCount,
            $nodes.Count)
    }

    $prefix = '$(LegacyResSourceRoot)\'
    $names = New-Object 'System.Collections.Generic.List[string]'
    $seen = New-Object 'System.Collections.Generic.HashSet[string]' (
        [StringComparer]::OrdinalIgnoreCase)
    foreach ($node in $nodes) {
        $include = [string] $node.Include
        if (-not $include.StartsWith($prefix, [StringComparison]::Ordinal)) {
            throw "LegacyPackagedResAsset is outside the closed Res projection: $include"
        }

        $name = $include.Substring($prefix.Length)
        if ([string]::IsNullOrWhiteSpace($name) -or
            $name.IndexOfAny(@(
                [IO.Path]::DirectorySeparatorChar,
                [IO.Path]::AltDirectorySeparatorChar)) -ge 0 -or
            $name -eq '.' -or
            $name -eq '..' -or
            [IO.Path]::GetFileName($name) -ne $name) {
            throw "LegacyPackagedResAsset must be a single Res file name: $include"
        }

        Assert-SafePayloadRelativePath -RelativePath ('Res/' + $name)
        if (-not $seen.Add($name)) {
            throw "Duplicate LegacyPackagedResAsset declaration: $name"
        }

        $names.Add($name)
    }

    return @($names.ToArray() | Sort-Object)
}

function Convert-BytesToUpperHex {
    param(
        [Parameter(Mandatory = $true)]
        [byte[]] $Bytes
    )

    return ([BitConverter]::ToString($Bytes)).Replace('-', '')
}

function Copy-ArchiveEntryToNewFile {
    param(
        [Parameter(Mandatory = $true)]
        [IO.Compression.ZipArchiveEntry] $Entry,

        [Parameter(Mandatory = $true)]
        [string] $DestinationPath
    )

    $entryStream = $Entry.Open()
    $destinationStream = New-Object IO.FileStream(
        $DestinationPath,
        [IO.FileMode]::CreateNew,
        [IO.FileAccess]::Write,
        [IO.FileShare]::None)
    try {
        $buffer = New-Object byte[] 81920
        $written = [long] 0
        while (($read = $entryStream.Read($buffer, 0, $buffer.Length)) -gt 0) {
            if ($written -gt ([long] $Entry.Length - [long] $read)) {
                throw "Archive entry expanded beyond its declared length: $($Entry.FullName)"
            }
            $destinationStream.Write($buffer, 0, $read)
            $written += [long] $read
        }
        if ($written -ne [long] $Entry.Length) {
            throw "Archive entry length changed during extraction: $($Entry.FullName)"
        }
        $destinationStream.Flush()
    }
    finally {
        $destinationStream.Dispose()
        $entryStream.Dispose()
    }
}

function Get-RequiredJsonProperty {
    param(
        [Parameter(Mandatory = $true)]
        [object] $Object,

        [Parameter(Mandatory = $true)]
        [string] $Name
    )

    $property = $Object.PSObject.Properties[$Name]
    if ($null -eq $property) {
        throw "Bundle manifest is missing required property: $Name"
    }

    return $property.Value
}

function ConvertTo-ManifestInteger {
    param(
        [Parameter(Mandatory = $true)]
        [object] $Value,

        [Parameter(Mandatory = $true)]
        [string] $Name
    )

    if (-not ($Value -is [byte] -or
        $Value -is [int16] -or
        $Value -is [int32] -or
        $Value -is [int64])) {
        throw "Bundle manifest property must be an integer: $Name"
    }

    $result = [long] $Value
    if ($result -lt 0) {
        throw "Bundle manifest property cannot be negative: $Name"
    }
    return $result
}

function Assert-RepositorySupportsLocalProps {
    param(
        [Parameter(Mandatory = $true)]
        [string] $Root
    )

    $gitIgnorePath = Join-Path $Root '.gitignore'
    $directoryBuildPropsPath = Join-Path $Root 'Directory.Build.props'
    if (-not (Test-Path -LiteralPath $gitIgnorePath -PathType Leaf)) {
        throw "Repository .gitignore was not found: $gitIgnorePath"
    }
    if (-not (Test-Path -LiteralPath $directoryBuildPropsPath -PathType Leaf)) {
        throw "Directory.Build.props was not found: $directoryBuildPropsPath"
    }

    $ignoreLines = @([IO.File]::ReadAllLines($gitIgnorePath))
    $hasLocalPropsIgnore = $false
    foreach ($line in $ignoreLines) {
        $trimmed = $line.Trim()
        if ($trimmed -match '^/?Directory\.Build\.local\.props$') {
            $hasLocalPropsIgnore = $true
        }
        if ($trimmed -match '^!/?Directory\.Build\.local\.props$') {
            throw 'Directory.Build.local.props is explicitly unignored by .gitignore.'
        }
    }
    if (-not $hasLocalPropsIgnore) {
        throw 'Directory.Build.local.props must be ignored before runtime initialization.'
    }

    [xml] $directoryBuildPropsXml = Get-Content `
        -LiteralPath $directoryBuildPropsPath `
        -Raw `
        -Encoding UTF8
    $imports = @($directoryBuildPropsXml.SelectNodes(
        "/*[local-name()='Project']/*[local-name()='Import']"))
    $matchingImports = @($imports | Where-Object {
        $projectAttribute = [string] $_.Project
        $projectAttribute.Replace('\', '/').EndsWith(
            'Directory.Build.local.props',
            [StringComparison]::OrdinalIgnoreCase)
    })
    if ($matchingImports.Count -eq 0) {
        throw 'Directory.Build.props must conditionally import Directory.Build.local.props.'
    }
}

function Assert-InstalledBundle {
    param(
        [Parameter(Mandatory = $true)]
        [string] $InstalledRoot,

        [Parameter(Mandatory = $true)]
        [System.Collections.Generic.Dictionary[string, object]] $ManifestRecords,

        [Parameter(Mandatory = $true)]
        [string] $ExpectedManifestHash
    )

    if (-not (Test-Path -LiteralPath $InstalledRoot -PathType Container)) {
        throw "Installed runtime bundle directory was not found: $InstalledRoot"
    }
    Assert-NoReparsePointInExistingAncestry `
        -Path $InstalledRoot `
        -Description 'Installed runtime bundle'

    $expectedPaths = New-Object 'System.Collections.Generic.HashSet[string]' (
        [StringComparer]::OrdinalIgnoreCase)
    foreach ($manifestPath in $ManifestRecords.Keys) {
        $expectedPaths.Add($manifestPath) | Out-Null
    }
    $expectedPaths.Add($manifestFileName) | Out-Null

    $actualPaths = New-Object 'System.Collections.Generic.HashSet[string]' (
        [StringComparer]::OrdinalIgnoreCase)
    $pending = New-Object 'System.Collections.Generic.Queue[string]'
    $pending.Enqueue($InstalledRoot)
    while ($pending.Count -gt 0) {
        $directory = $pending.Dequeue()
        foreach ($child in @(Get-ChildItem -LiteralPath $directory -Force)) {
            if (($child.Attributes -band [IO.FileAttributes]::ReparsePoint) -ne 0) {
                throw "Installed runtime bundle contains a reparse point: $($child.FullName)"
            }

            if ($child.PSIsContainer) {
                $pending.Enqueue($child.FullName)
                continue
            }

            $relativePath = $child.FullName.Substring(
                $InstalledRoot.TrimEnd('\').Length + 1).Replace('\', '/')
            if (-not $actualPaths.Add($relativePath)) {
                throw "Installed runtime bundle contains a duplicate path: $relativePath"
            }
            if (-not $expectedPaths.Contains($relativePath)) {
                throw "Installed runtime bundle contains an unexpected file: $relativePath"
            }
        }
    }

    if ($actualPaths.Count -ne $expectedPaths.Count) {
        $missing = @($expectedPaths | Where-Object { -not $actualPaths.Contains($_) })
        throw "Installed runtime bundle is missing files: $($missing -join ', ')"
    }

    foreach ($manifestPath in $ManifestRecords.Keys) {
        $record = $ManifestRecords[$manifestPath]
        $nativeRelativePath = $manifestPath.Replace(
            '/',
            [IO.Path]::DirectorySeparatorChar)
        $installedFilePath = Join-Path $InstalledRoot $nativeRelativePath
        $installedFile = Get-Item -LiteralPath $installedFilePath -Force
        if ([long] $installedFile.Length -ne [long] $record.length) {
            throw "Installed runtime bundle file length mismatch: $manifestPath"
        }
        $actualHash = (Get-FileHash `
            -LiteralPath $installedFilePath `
            -Algorithm SHA256).Hash.ToUpperInvariant()
        if (-not $actualHash.Equals(
                [string] $record.sha256,
                [StringComparison]::Ordinal)) {
            throw "Installed runtime bundle file hash mismatch: $manifestPath"
        }
    }

    $installedManifestPath = Join-Path $InstalledRoot $manifestFileName
    $actualManifestHash = (Get-FileHash `
        -LiteralPath $installedManifestPath `
        -Algorithm SHA256).Hash.ToUpperInvariant()
    if (-not $actualManifestHash.Equals(
            $ExpectedManifestHash,
            [StringComparison]::Ordinal)) {
        throw 'Installed runtime bundle manifest does not match the verified archive manifest.'
    }
}

function Ensure-LocalBuildProps {
    param(
        [Parameter(Mandatory = $true)]
        [string] $Root,

        [Parameter(Mandatory = $true)]
        [string] $InstalledRoot
    )

    $propsPath = Join-Path $Root 'Directory.Build.local.props'
    if (Test-Path -LiteralPath $propsPath) {
        if (-not (Test-Path -LiteralPath $propsPath -PathType Leaf)) {
            throw "Local build props path is not a regular file: $propsPath"
        }
        Assert-NoReparsePointInExistingAncestry `
            -Path $propsPath `
            -Description 'Local build props'
        [xml] $existingXml = Get-Content -LiteralPath $propsPath -Raw -Encoding UTF8
        $projectNodes = @($existingXml.SelectNodes(
            "/*[local-name()='Project']"))
        $propertyGroups = @($existingXml.SelectNodes(
            "/*[local-name()='Project']/*[local-name()='PropertyGroup']"))
        $rootNodes = @($existingXml.SelectNodes(
            "/*[local-name()='Project']/*[local-name()='PropertyGroup']/*[local-name()='LegacyRuntimeSourceRoot']"))
        $modeNodes = @($existingXml.SelectNodes(
            "/*[local-name()='Project']/*[local-name()='PropertyGroup']/*[local-name()='LegacyRuntimeAssetsMode']"))
        if ($projectNodes.Count -ne 1 -or
            $projectNodes[0].Attributes.Count -ne 0 -or
            @($projectNodes[0].ChildNodes | Where-Object {
                $_.NodeType -eq [Xml.XmlNodeType]::Element
            }).Count -ne 1 -or
            $propertyGroups.Count -ne 1 -or
            $rootNodes.Count -ne 1 -or
            $modeNodes.Count -ne 1 -or
            $propertyGroups[0].Attributes.Count -ne 0 -or
            @($propertyGroups[0].ChildNodes | Where-Object {
                $_.NodeType -eq [Xml.XmlNodeType]::Element
            }).Count -ne 2 -or
            $rootNodes[0].Attributes.Count -ne 0 -or
            $modeNodes[0].Attributes.Count -ne 0) {
            throw 'Existing Directory.Build.local.props is conditional, duplicated, or differs from the generated shape.'
        }

        $existingRoot = Get-NormalizedFullPath -Path ([string] $rootNodes[0].InnerText)
        if (-not $existingRoot.Equals(
                (Get-NormalizedFullPath -Path $InstalledRoot),
                [StringComparison]::OrdinalIgnoreCase) -or
            -not ([string] $modeNodes[0].InnerText).Equals(
                'Required',
                [StringComparison]::Ordinal)) {
            throw 'Existing Directory.Build.local.props selects a different runtime bundle or mode; it was not overwritten.'
        }

        return $propsPath
    }

    $temporaryPropsPath = Join-Path $Root (
        '.Directory.Build.local.props.' + [Guid]::NewGuid().ToString('N') + '.tmp')
    try {
        $settings = New-Object Xml.XmlWriterSettings
        $settings.Encoding = New-Object Text.UTF8Encoding($false)
        $settings.Indent = $true
        $settings.NewLineChars = [Environment]::NewLine
        $settings.NewLineHandling = [Xml.NewLineHandling]::Replace
        $writer = [Xml.XmlWriter]::Create($temporaryPropsPath, $settings)
        try {
            $writer.WriteStartDocument()
            $writer.WriteStartElement('Project')
            $writer.WriteStartElement('PropertyGroup')
            $writer.WriteElementString(
                'LegacyRuntimeSourceRoot',
                (Get-NormalizedFullPath -Path $InstalledRoot))
            $writer.WriteElementString('LegacyRuntimeAssetsMode', 'Required')
            $writer.WriteEndElement()
            $writer.WriteEndElement()
            $writer.WriteEndDocument()
        }
        finally {
            $writer.Dispose()
        }

        Assert-NoReparsePointInExistingAncestry `
            -Path $temporaryPropsPath `
            -Description 'Temporary local build props'
        [IO.File]::Move($temporaryPropsPath, $propsPath)
    }
    finally {
        if (Test-Path -LiteralPath $temporaryPropsPath) {
            Remove-Item -LiteralPath $temporaryPropsPath -Force
        }
    }

    return $propsPath
}

if ([string]::IsNullOrWhiteSpace($RepositoryRoot)) {
    $repositoryPath = Get-NormalizedFullPath -Path (Join-Path $PSScriptRoot '..')
}
else {
    $repositoryPath = Get-NormalizedFullPath -Path $RepositoryRoot
}
if (-not (Test-Path -LiteralPath $repositoryPath -PathType Container)) {
    throw "Repository root was not found: $repositoryPath"
}
Assert-NoReparsePointInExistingAncestry `
    -Path $repositoryPath `
    -Description 'Repository root'
Assert-RepositorySupportsLocalProps -Root $repositoryPath

$projectPath = Join-Path $repositoryPath 'src\MBN_STOCK_WEBVIEW.LegacyParityApp\MBN_STOCK_WEBVIEW.LegacyParityApp.csproj'
$resAssetNames = @(Get-LegacyPackagedResAssetNames -ProjectPath $projectPath)
$allowedResPaths = New-Object 'System.Collections.Generic.Dictionary[string, string]' (
    [StringComparer]::OrdinalIgnoreCase)
foreach ($resAssetName in $resAssetNames) {
    $allowedResPaths.Add(('Res/' + $resAssetName), ('Res/' + $resAssetName))
}

$archivePath = Get-NormalizedFullPath -Path $ZipPath
if (-not (Test-Path -LiteralPath $archivePath -PathType Leaf)) {
    throw "Legacy runtime bundle archive was not found: $archivePath"
}
Assert-NoReparsePointInExistingAncestry `
    -Path $archivePath `
    -Description 'Legacy runtime bundle archive'

$normalizedExpectedHash = $ExpectedSha256.Trim().ToUpperInvariant()
if ($normalizedExpectedHash -notmatch '^[0-9A-F]{64}$') {
    throw 'ExpectedSha256 must be a separately delivered 64-character SHA-256 value.'
}

Add-Type -AssemblyName System.IO.Compression
Add-Type -AssemblyName System.IO.Compression.FileSystem
$archiveStream = [IO.File]::Open(
    $archivePath,
    [IO.FileMode]::Open,
    [IO.FileAccess]::Read,
    [IO.FileShare]::Read)
$archive = $null
$stagingRoot = $null
try {
    $sha256 = [Security.Cryptography.SHA256]::Create()
    try {
        $actualArchiveHash = Convert-BytesToUpperHex -Bytes (
            $sha256.ComputeHash($archiveStream))
    }
    finally {
        $sha256.Dispose()
    }
    if (-not $actualArchiveHash.Equals(
            $normalizedExpectedHash,
            [StringComparison]::Ordinal)) {
        throw ("Legacy runtime bundle SHA-256 mismatch. Expected {0}; actual {1}." -f
            $normalizedExpectedHash,
            $actualArchiveHash)
    }
    $archiveStream.Position = 0
    $archive = New-Object IO.Compression.ZipArchive(
        $archiveStream,
        [IO.Compression.ZipArchiveMode]::Read,
        $true)

    $entries = @($archive.Entries)
    if ($entries.Count -eq 0 -or $entries.Count -gt $maximumEntryCount) {
        throw "Legacy runtime bundle has an invalid entry count: $($entries.Count)"
    }

    $canonicalEntries = New-Object 'System.Collections.Generic.Dictionary[string, bool]' (
        [StringComparer]::OrdinalIgnoreCase)
    $payloadEntries = New-Object 'System.Collections.Generic.Dictionary[string, object]' (
        [StringComparer]::OrdinalIgnoreCase)
    $directoryEntries = New-Object 'System.Collections.Generic.List[object]'
    $manifestEntry = $null
    $totalPayloadLength = [long] 0

    foreach ($entry in $entries) {
        if (Test-ArchiveEntryIsReparsePoint -Entry $entry) {
            throw "Archive entry is a symbolic link or reparse point: $($entry.FullName)"
        }

        $isDirectory = $entry.FullName.EndsWith('/', [StringComparison]::Ordinal)
        $canonicalPath = Assert-SafeArchiveEntryPath `
            -EntryPath $entry.FullName `
            -IsDirectory $isDirectory
        if ($canonicalEntries.ContainsKey($canonicalPath)) {
            throw "Archive contains a duplicate or file/directory-colliding path: $canonicalPath"
        }
        $canonicalEntries.Add($canonicalPath, $isDirectory)

        if ($isDirectory) {
            if ([long] $entry.Length -ne 0 -or
                -not ($canonicalPath.Equals(
                        'Cuts',
                        [StringComparison]::Ordinal) -or
                    $canonicalPath.Equals(
                        'Res',
                        [StringComparison]::Ordinal) -or
                    $canonicalPath.StartsWith(
                        'Cuts/',
                        [StringComparison]::Ordinal))) {
                throw "Archive contains an unexpected directory: $($entry.FullName)"
            }
            $directoryEntries.Add($entry)
            continue
        }

        if ([long] $entry.Length -gt $maximumFileLength) {
            throw "Archive entry exceeds the per-file size limit: $canonicalPath"
        }

        if ($canonicalPath.Equals(
                $manifestFileName,
                [StringComparison]::Ordinal)) {
            if ($null -ne $manifestEntry) {
                throw 'Archive contains more than one bundle manifest.'
            }
            if ([long] $entry.Length -gt $maximumManifestLength) {
                throw 'Bundle manifest exceeds the size limit.'
            }
            $manifestEntry = $entry
            continue
        }

        if ($canonicalPath.StartsWith('Cuts/', [StringComparison]::Ordinal)) {
            Assert-SafePayloadRelativePath -RelativePath $canonicalPath
        }
        elseif ($canonicalPath.StartsWith('Res/', [StringComparison]::Ordinal)) {
            Assert-SafePayloadRelativePath -RelativePath $canonicalPath
            if (-not $allowedResPaths.ContainsKey($canonicalPath) -or
                $allowedResPaths[$canonicalPath] -cne $canonicalPath) {
                throw "Archive contains a Res file outside the project allowlist: $canonicalPath"
            }
        }
        else {
            throw "Archive contains a file outside Cuts/Res: $canonicalPath"
        }

        if ($totalPayloadLength -gt ($maximumPayloadLength - [long] $entry.Length)) {
            throw 'Archive payload exceeds the total size limit.'
        }
        $totalPayloadLength += [long] $entry.Length
        $payloadEntries.Add($canonicalPath, $entry)
    }

    foreach ($canonicalPath in $canonicalEntries.Keys) {
        $segments = @($canonicalPath.Split('/'))
        for ($segmentIndex = 1; $segmentIndex -lt $segments.Count; $segmentIndex++) {
            $ancestorPath = ($segments[0..($segmentIndex - 1)] -join '/')
            if ($canonicalEntries.ContainsKey($ancestorPath) -and
                -not $canonicalEntries[$ancestorPath]) {
                throw ("Archive file is also the parent of another entry: {0}" -f
                    $ancestorPath)
            }
        }
    }

    if ($null -eq $manifestEntry) {
        throw 'Archive does not contain LegacyRuntimeBundle.manifest.json.'
    }

    $manifestStream = $manifestEntry.Open()
    $manifestMemory = New-Object IO.MemoryStream
    try {
        $manifestStream.CopyTo($manifestMemory)
        $manifestBytes = $manifestMemory.ToArray()
    }
    finally {
        $manifestMemory.Dispose()
        $manifestStream.Dispose()
    }
    $manifestSha = [Security.Cryptography.SHA256]::Create()
    try {
        $manifestHash = Convert-BytesToUpperHex -Bytes (
            $manifestSha.ComputeHash($manifestBytes))
    }
    finally {
        $manifestSha.Dispose()
    }
    $strictUtf8 = New-Object Text.UTF8Encoding($false, $true)
    $manifestJson = $strictUtf8.GetString($manifestBytes)
    if ($manifestJson.Length -gt 0 -and $manifestJson[0] -eq [char] 0xFEFF) {
        $manifestJson = $manifestJson.Substring(1)
    }
    try {
        $manifest = $manifestJson | ConvertFrom-Json
    }
    catch {
        throw "Bundle manifest is not valid UTF-8 JSON: $($_.Exception.Message)"
    }

    $schemaVersion = ConvertTo-ManifestInteger `
        -Value (Get-RequiredJsonProperty -Object $manifest -Name 'schemaVersion') `
        -Name 'schemaVersion'
    $bundleType = [string] (
        Get-RequiredJsonProperty -Object $manifest -Name 'bundleType')
    $manifestFileCount = ConvertTo-ManifestInteger `
        -Value (Get-RequiredJsonProperty -Object $manifest -Name 'fileCount') `
        -Name 'fileCount'
    $manifestCutsFileCount = ConvertTo-ManifestInteger `
        -Value (Get-RequiredJsonProperty -Object $manifest -Name 'cutsFileCount') `
        -Name 'cutsFileCount'
    $manifestResFileCount = ConvertTo-ManifestInteger `
        -Value (Get-RequiredJsonProperty -Object $manifest -Name 'resFileCount') `
        -Name 'resFileCount'
    $manifestFileRecords = @(
        Get-RequiredJsonProperty -Object $manifest -Name 'files')
    if ($schemaVersion -ne 1 -or
        -not $bundleType.Equals(
            'MBN_STOCK_WEBVIEW.LegacyRuntimeBundle',
            [StringComparison]::Ordinal)) {
        throw 'Bundle manifest schema or bundle type is unsupported.'
    }

    $manifestRecords = New-Object 'System.Collections.Generic.Dictionary[string, object]' (
        [StringComparer]::OrdinalIgnoreCase)
    $actualCutsFileCount = 0
    $actualResFileCount = 0
    foreach ($record in $manifestFileRecords) {
        $recordPath = [string] (
            Get-RequiredJsonProperty -Object $record -Name 'path')
        $recordLength = ConvertTo-ManifestInteger `
            -Value (Get-RequiredJsonProperty -Object $record -Name 'length') `
            -Name ('files[{0}].length' -f $recordPath)
        $recordHash = [string] (
            Get-RequiredJsonProperty -Object $record -Name 'sha256')
        if ($recordHash -notmatch '^[0-9A-Fa-f]{64}$') {
            throw "Bundle manifest has an invalid file SHA-256: $recordPath"
        }
        $recordHash = $recordHash.ToUpperInvariant()

        $canonicalRecordPath = Assert-SafeArchiveEntryPath `
            -EntryPath $recordPath `
            -IsDirectory $false
        if ($canonicalRecordPath -cne $recordPath) {
            throw "Bundle manifest path is not canonical: $recordPath"
        }
        Assert-SafePayloadRelativePath -RelativePath $recordPath
        if ($manifestRecords.ContainsKey($recordPath)) {
            throw "Bundle manifest contains a duplicate path: $recordPath"
        }
        $manifestRecords.Add(
            $recordPath,
            [pscustomobject]@{
                length = [long] $recordLength
                sha256 = $recordHash
            })
        if (-not $payloadEntries.ContainsKey($recordPath) -or
            $payloadEntries[$recordPath].FullName -cne $recordPath) {
            throw "Bundle manifest references a file absent from the archive: $recordPath"
        }
        if ([long] $payloadEntries[$recordPath].Length -ne [long] $recordLength) {
            throw "Bundle manifest length does not match the archive: $recordPath"
        }

        if ($recordPath.StartsWith('Cuts/', [StringComparison]::Ordinal)) {
            $actualCutsFileCount++
        }
        elseif ($recordPath.StartsWith('Res/', [StringComparison]::Ordinal)) {
            if (-not $allowedResPaths.ContainsKey($recordPath) -or
                $allowedResPaths[$recordPath] -cne $recordPath) {
                throw "Bundle manifest contains a Res file outside the project allowlist: $recordPath"
            }
            $actualResFileCount++
        }
        else {
            throw "Bundle manifest contains a file outside Cuts/Res: $recordPath"
        }
    }

    if ($manifestRecords.Count -ne $payloadEntries.Count -or
        $manifestFileCount -ne $manifestRecords.Count -or
        $manifestCutsFileCount -ne $actualCutsFileCount -or
        $manifestResFileCount -ne $actualResFileCount -or
        $actualResFileCount -ne $expectedResAssetCount -or
        $actualCutsFileCount -le 0) {
        throw 'Bundle manifest counts or file set do not match the closed archive payload.'
    }
    foreach ($payloadPath in $payloadEntries.Keys) {
        if (-not $manifestRecords.ContainsKey($payloadPath)) {
            throw "Archive contains a payload file absent from the manifest: $payloadPath"
        }
    }

    $knownLocalAppData = [Environment]::GetFolderPath(
        [Environment+SpecialFolder]::LocalApplicationData)
    if ([string]::IsNullOrWhiteSpace($knownLocalAppData)) {
        throw 'The Windows LocalApplicationData known folder is unavailable.'
    }
    $localAppDataRoot = Get-NormalizedFullPath -Path $knownLocalAppData
    $bundleStorageRoot = Join-Path $localAppDataRoot 'MBN_STOCK_WEBVIEW\RuntimeBundles'
    Assert-NoReparsePointInExistingAncestry `
        -Path $bundleStorageRoot `
        -Description 'Runtime bundle storage'
    [IO.Directory]::CreateDirectory($bundleStorageRoot) | Out-Null
    Assert-NoReparsePointInExistingAncestry `
        -Path $bundleStorageRoot `
        -Description 'Runtime bundle storage'

    $installedRoot = Join-Path $bundleStorageRoot $normalizedExpectedHash
    if (Test-Path -LiteralPath $installedRoot) {
        Assert-InstalledBundle `
            -InstalledRoot $installedRoot `
            -ManifestRecords $manifestRecords `
            -ExpectedManifestHash $manifestHash
    }
    else {
        $stagingRoot = Join-Path $bundleStorageRoot (
            '.installing-' + [Guid]::NewGuid().ToString('N'))
        [IO.Directory]::CreateDirectory($stagingRoot) | Out-Null
        Assert-NoReparsePointInExistingAncestry `
            -Path $stagingRoot `
            -Description 'Runtime bundle staging directory'

        foreach ($directoryEntry in @($directoryEntries.ToArray() | Sort-Object FullName)) {
            $directoryPath = $directoryEntry.FullName.TrimEnd('/').Replace(
                '/',
                [IO.Path]::DirectorySeparatorChar)
            $destinationDirectory = Join-Path $stagingRoot $directoryPath
            if (-not (Test-PathIsWithin `
                    -Root $stagingRoot `
                    -Candidate $destinationDirectory)) {
                throw "Archive directory escaped the staging root: $($directoryEntry.FullName)"
            }
            [IO.Directory]::CreateDirectory($destinationDirectory) | Out-Null
            Assert-NoReparsePointInExistingAncestry `
                -Path $destinationDirectory `
                -Description 'Extracted runtime directory'
        }

        $entriesToExtract = New-Object 'System.Collections.Generic.List[object]'
        foreach ($payloadEntry in $payloadEntries.Values) {
            $entriesToExtract.Add($payloadEntry)
        }
        $entriesToExtract.Add($manifestEntry)
        foreach ($entry in @($entriesToExtract.ToArray() | Sort-Object FullName)) {
            $nativeRelativePath = $entry.FullName.Replace(
                '/',
                [IO.Path]::DirectorySeparatorChar)
            $destinationPath = Join-Path $stagingRoot $nativeRelativePath
            if (-not (Test-PathIsWithin `
                    -Root $stagingRoot `
                    -Candidate $destinationPath)) {
                throw "Archive file escaped the staging root: $($entry.FullName)"
            }
            $destinationParent = [IO.Path]::GetDirectoryName($destinationPath)
            [IO.Directory]::CreateDirectory($destinationParent) | Out-Null
            Assert-NoReparsePointInExistingAncestry `
                -Path $destinationParent `
                -Description 'Extracted runtime file parent'

            Copy-ArchiveEntryToNewFile `
                -Entry $entry `
                -DestinationPath $destinationPath
        }

        Assert-InstalledBundle `
            -InstalledRoot $stagingRoot `
            -ManifestRecords $manifestRecords `
            -ExpectedManifestHash $manifestHash
        try {
            [IO.Directory]::Move($stagingRoot, $installedRoot)
            $stagingRoot = $null
        }
        catch {
            if (Test-Path -LiteralPath $installedRoot -PathType Container) {
                Assert-InstalledBundle `
                    -InstalledRoot $installedRoot `
                    -ManifestRecords $manifestRecords `
                    -ExpectedManifestHash $manifestHash
            }
            else {
                throw
            }
        }
    }

    $localPropsPath = Ensure-LocalBuildProps `
        -Root $repositoryPath `
        -InstalledRoot $installedRoot

    [pscustomobject]@{
        InstalledRoot = $installedRoot
        ArchivePath = $archivePath
        ArchiveSha256 = $actualArchiveHash
        ManifestSha256 = $manifestHash
        FileCount = [int] $manifestRecords.Count
        CutsFileCount = [int] $actualCutsFileCount
        ResFileCount = [int] $actualResFileCount
        LocalBuildPropsPath = $localPropsPath
    }
}
finally {
    if ($null -ne $archive) {
        $archive.Dispose()
    }
    $archiveStream.Dispose()
    if (-not [string]::IsNullOrWhiteSpace($stagingRoot) -and
        (Test-Path -LiteralPath $stagingRoot)) {
        if (-not (Test-PathIsWithin `
                -Root $bundleStorageRoot `
                -Candidate $stagingRoot) -or
            -not ([IO.Path]::GetFileName($stagingRoot)).StartsWith(
                '.installing-',
                [StringComparison]::Ordinal)) {
            throw "Refusing to clean an unexpected staging path: $stagingRoot"
        }
        Assert-NoReparsePointInTree `
            -Root $stagingRoot `
            -Description 'Runtime bundle staging cleanup'
        Remove-Item -LiteralPath $stagingRoot -Recurse -Force
    }
}
