[CmdletBinding(SupportsShouldProcess = $true)]
|
param(
|
[Parameter(Mandatory = $true)]
|
[string]$ObservedExtensionId,
|
[Parameter(Mandatory = $true)]
|
[string]$HostExecutable,
|
[Parameter(Mandatory = $true)]
|
[string]$ConfigFile,
|
[Parameter(Mandatory = $true)]
|
[string]$InstallRoot,
|
[Parameter(Mandatory = $true)]
|
[string]$SourceArtifactManifest,
|
[Parameter(Mandatory = $true)]
|
[string]$BuildArtifactManifest,
|
[Parameter(Mandatory = $true)]
|
[string]$ApprovedSourceReceipt,
|
[Parameter(Mandatory = $true)]
|
[string]$ApprovedBuildReceipt,
|
[switch]$Install,
|
[Parameter(DontShow = $true)]
|
[switch]$TestFileRegistryProvider,
|
[Parameter(DontShow = $true)]
|
[string]$TestRegistryRoot,
|
[Parameter(DontShow = $true)]
|
[string]$TestPreviousInstallRoot,
|
[Parameter(DontShow = $true)]
|
[string]$TestPreviousArtifactReceipt,
|
[Parameter(DontShow = $true)]
|
[ValidateSet('none', 'after-root', 'after-payload', 'after-config', 'after-manifest', 'after-registry-key', 'after-registry-value', 'after-registry-value-mixed')]
|
[string]$InjectFailure = 'none'
|
)
|
|
$ErrorActionPreference = 'Stop'
|
$expectedId = 'oidmclckpdmpabbfedplkbdplmfcenbb'
|
$expectedOrigin = 'chrome-extension://oidmclckpdmpabbfedplkbdplmfcenbb/'
|
$expectedHostName = 'com.project_info.bili_auth_ingress'
|
$expectedPublicDerHash = 'E83C2B2AF3CF011543FBA13FBC524D1122EEA68548F9F27B9F7A82B5D594666C'
|
$expectedExtensionBuild = 'project-info-bili-auth-ingress/1.2.25+20260829.generic.v027'
|
$expectedHostBuild = 'project-info-bili-auth-native-host/1.2.25+20260829.generic.v027'
|
$expectedHostExecutable = 'project-info-bili-auth-native-host.exe'
|
$expectedPreviousExtensionBuild = 'project-info-bili-auth-ingress/1.2.24+20260829.generic.v026'
|
$expectedPreviousVersion = '1.2.24+20260829.generic.v026'
|
$expectedPreviousManifestBytes = 381
|
$expectedPreviousManifestSha256 = '9FFAC5073A5839F030ED321840C5715ECBE2CC7276DD057A7236B3CF78AA5C38'
|
$expectedPreviousHostBytes = 21452778
|
$expectedPreviousHostSha256 = '24E0C15B9E8269F55F25E55A0614DD96BEF58CB0F7EE069C41F86AF95CD19762'
|
$expectedPreviousConfigBytes = 1870
|
$expectedPreviousConfigSha256 = 'ACA285C700CD970A846A824BD32E6826195B0204640F8445C288C7476EC475E3'
|
$publicKey = 'MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAt2dT1HGYaI0DXM7zZwOTNBWXTKlMBJMpyDVjRUc+v6bUotmLyoraC+ay2scy9UQluSZVYq0tS8qvQNNvuZOlc5w2bOExm4TH2IIKvaVO8nVthHBnNz2kXdiM8ItN0vPZEmS+8gpTCI1+6wPTuUglMoXpqYBYhii8fJ5RkENRF3PRJBBigGt8soqdBFRY1QZUmpQv9dYw4dRq4L2C4QtBgClUg4bQpuCppiVZ9LHbePi9IAjc9r9R93KLzpaBuXdJpfVRE5w/6YHnxP8ovXxBdl7XktmrdH3xj7mWT7Q7ZBxkDNwn2RkruD45XgDD3yuNxOYSkLFMkseN+Ua69gSS4wIDAQAB'
|
|
function Stop-Injected([string]$Point) {
|
if ($InjectFailure -ceq $Point) {
|
throw "Injected installer failure: $Point"
|
}
|
}
|
|
function Get-StrictJson([string]$Path) {
|
$text = [System.IO.File]::ReadAllText($Path, [System.Text.UTF8Encoding]::new($false, $true))
|
return $text | ConvertFrom-Json
|
}
|
|
function Test-PathWithin([string]$Candidate, [string]$Root) {
|
$prefix = $Root.TrimEnd([System.IO.Path]::DirectorySeparatorChar) + [System.IO.Path]::DirectorySeparatorChar
|
return $Candidate.Equals($Root, [StringComparison]::OrdinalIgnoreCase) -or
|
$Candidate.StartsWith($prefix, [StringComparison]::OrdinalIgnoreCase)
|
}
|
|
function Get-ExternalReceipt([string]$Path, [string[]]$ForbiddenRoots) {
|
$resolved = (Resolve-Path -LiteralPath $Path).Path
|
$item = Get-Item -LiteralPath $resolved
|
if ($item.PSIsContainer -or ($item.Attributes -band [IO.FileAttributes]::ReparsePoint)) {
|
throw 'Approval receipt must be a regular non-reparse file.'
|
}
|
foreach ($forbidden in $ForbiddenRoots) {
|
if (Test-PathWithin $resolved $forbidden) {
|
throw 'Approval receipt must be outside the mutable source and build trees.'
|
}
|
}
|
return [pscustomobject]@{ Path = $resolved; Value = (Get-StrictJson $resolved) }
|
}
|
|
function Assert-ExactSourceTree([string]$Root, [string]$ManifestPath, [string[]]$ExpectedFiles) {
|
$rootItem = Get-Item -LiteralPath $Root
|
if ($rootItem.Attributes -band [IO.FileAttributes]::ReparsePoint) {
|
throw 'Source root must not be a reparse path.'
|
}
|
$actualFiles = @()
|
foreach ($item in @(Get-ChildItem -LiteralPath $Root -Force -Recurse)) {
|
if ($item.Attributes -band [IO.FileAttributes]::ReparsePoint) {
|
throw 'Source tree contains a reparse path.'
|
}
|
if (-not $item.PSIsContainer -and $item.FullName -cne $ManifestPath) {
|
$actualFiles += $item.FullName.Substring($Root.Length + 1).Replace('\', '/')
|
}
|
}
|
if (Compare-Object -CaseSensitive ($ExpectedFiles | Sort-Object) @($actualFiles | Sort-Object)) {
|
throw 'Actual source tree file set does not exactly match the approved manifest.'
|
}
|
}
|
|
function Assert-ExactSourceSnapshot([string]$Root, [string]$ManifestPath, [string[]]$ExpectedFiles, [object]$Manifest) {
|
Assert-ExactSourceTree $Root $ManifestPath $ExpectedFiles
|
foreach ($entry in $Manifest.files) {
|
if ($entry.path -notmatch '^[A-Za-z0-9._/-]+$' -or $entry.path.Contains('..') -or
|
$entry.sha256 -notmatch '^[A-F0-9]{64}$' -or $entry.bytes -lt 1) {
|
throw 'Source artifact manifest contains an invalid entry.'
|
}
|
$candidate = [System.IO.Path]::GetFullPath((Join-Path $Root $entry.path))
|
if (-not $candidate.StartsWith($Root + [System.IO.Path]::DirectorySeparatorChar, [StringComparison]::OrdinalIgnoreCase)) {
|
throw 'Source artifact path escaped its root.'
|
}
|
$item = Get-Item -LiteralPath $candidate
|
if (($item.Attributes -band [IO.FileAttributes]::ReparsePoint) -or
|
$item.Length -ne $entry.bytes -or
|
(Get-FileHash -Algorithm SHA256 -LiteralPath $candidate).Hash -cne $entry.sha256) {
|
throw 'Source artifact hash mismatch.'
|
}
|
}
|
}
|
|
function Copy-CreateNew([string]$Source, [string]$Destination) {
|
$input = [System.IO.File]::Open($Source, [IO.FileMode]::Open, [IO.FileAccess]::Read, [IO.FileShare]::Read)
|
try {
|
$output = [System.IO.File]::Open($Destination, [IO.FileMode]::CreateNew, [IO.FileAccess]::Write, [IO.FileShare]::None)
|
try {
|
$input.CopyTo($output)
|
$output.Flush($true)
|
} finally {
|
$output.Dispose()
|
}
|
} finally {
|
$input.Dispose()
|
}
|
}
|
|
function Write-Utf8CreateNew([string]$Path, [string]$Text) {
|
$bytes = [System.Text.UTF8Encoding]::new($false).GetBytes($Text)
|
$stream = [System.IO.File]::Open($Path, [IO.FileMode]::CreateNew, [IO.FileAccess]::Write, [IO.FileShare]::None)
|
try {
|
$stream.Write($bytes, 0, $bytes.Length)
|
$stream.Flush($true)
|
} finally {
|
$stream.Dispose()
|
}
|
}
|
|
function Get-FileIdentity([string]$Path) {
|
$item = Get-Item -LiteralPath $Path -Force
|
if ($item.PSIsContainer -or ($item.Attributes -band [IO.FileAttributes]::ReparsePoint)) {
|
throw 'Expected a regular non-reparse file.'
|
}
|
return [pscustomobject]@{
|
Bytes = $item.Length
|
Sha256 = (Get-FileHash -Algorithm SHA256 -LiteralPath $Path).Hash
|
}
|
}
|
|
function ConvertTo-StrictPositiveInt64 {
|
[CmdletBinding(PositionalBinding = $false)]
|
param(
|
[Parameter(Mandatory = $true)]
|
[AllowNull()]
|
[object]$Value,
|
|
[Parameter(Mandatory = $true)]
|
[ValidateSet(
|
'previous native host manifest bytes',
|
'previous Host executable bytes',
|
'previous config bytes'
|
)]
|
[string]$IdentityName
|
)
|
$isIntegralScalar =
|
$Value -is [byte] -or $Value -is [sbyte] -or
|
$Value -is [int16] -or $Value -is [uint16] -or
|
$Value -is [int32] -or $Value -is [uint32] -or
|
$Value -is [int64]
|
if (-not $isIntegralScalar -or [long]$Value -lt 1) {
|
throw "Pinned $IdentityName must be a positive integer scalar."
|
}
|
return [long]$Value
|
}
|
|
function ConvertTo-StrictSha256 {
|
[CmdletBinding(PositionalBinding = $false)]
|
param(
|
[Parameter(Mandatory = $true)]
|
[AllowNull()]
|
[object]$Value,
|
|
[Parameter(Mandatory = $true)]
|
[ValidateSet(
|
'previous native host manifest SHA-256',
|
'previous Host executable SHA-256',
|
'previous config SHA-256'
|
)]
|
[string]$IdentityName
|
)
|
if ($Value -isnot [string] -or $Value -cnotmatch '^[A-F0-9]{64}$') {
|
throw "Pinned $IdentityName must be an uppercase 64-character hexadecimal string."
|
}
|
return [string]$Value
|
}
|
|
function Assert-ExactPreviousInstall {
|
[CmdletBinding(PositionalBinding = $false)]
|
param(
|
[Parameter(Mandatory = $true)]
|
[string]$PreviousRoot,
|
|
[Parameter(Mandatory = $true)]
|
[AllowNull()]
|
[object]$ManifestBytes,
|
|
[Parameter(Mandatory = $true)]
|
[AllowNull()]
|
[object]$ManifestSha256,
|
|
[Parameter(Mandatory = $true)]
|
[AllowNull()]
|
[object]$HostBytes,
|
|
[Parameter(Mandatory = $true)]
|
[AllowNull()]
|
[object]$HostSha256,
|
|
[Parameter(Mandatory = $true)]
|
[AllowNull()]
|
[object]$ConfigBytes,
|
|
[Parameter(Mandatory = $true)]
|
[AllowNull()]
|
[object]$ConfigSha256
|
)
|
$pinnedManifestBytes = ConvertTo-StrictPositiveInt64 -Value $ManifestBytes -IdentityName 'previous native host manifest bytes'
|
$pinnedManifestSha256 = ConvertTo-StrictSha256 -Value $ManifestSha256 -IdentityName 'previous native host manifest SHA-256'
|
$pinnedHostBytes = ConvertTo-StrictPositiveInt64 -Value $HostBytes -IdentityName 'previous Host executable bytes'
|
$pinnedHostSha256 = ConvertTo-StrictSha256 -Value $HostSha256 -IdentityName 'previous Host executable SHA-256'
|
$pinnedConfigBytes = ConvertTo-StrictPositiveInt64 -Value $ConfigBytes -IdentityName 'previous config bytes'
|
$pinnedConfigSha256 = ConvertTo-StrictSha256 -Value $ConfigSha256 -IdentityName 'previous config SHA-256'
|
|
$rootItem = Get-Item -LiteralPath $PreviousRoot -Force
|
if (-not $rootItem.PSIsContainer -or ($rootItem.Attributes -band [IO.FileAttributes]::ReparsePoint)) {
|
throw 'Previous install root must be an ordinary non-reparse directory.'
|
}
|
$children = @(Get-ChildItem -LiteralPath $PreviousRoot -Force)
|
if ($children.Count -ne 3 -or @($children | Where-Object { $_.PSIsContainer }).Count -ne 0 -or
|
(Compare-Object -CaseSensitive @('config.json', 'native-host-manifest.json', $expectedHostExecutable) @($children.Name | Sort-Object))) {
|
throw 'Previous install root does not contain the exact three-file preimage.'
|
}
|
foreach ($child in $children) {
|
if ($child.Attributes -band [IO.FileAttributes]::ReparsePoint) {
|
throw 'Previous install contains a reparse path.'
|
}
|
}
|
$manifestPath = Join-Path $PreviousRoot 'native-host-manifest.json'
|
$hostPath = Join-Path $PreviousRoot $expectedHostExecutable
|
$configPath = Join-Path $PreviousRoot 'config.json'
|
$manifestIdentity = Get-FileIdentity $manifestPath
|
$hostIdentity = Get-FileIdentity $hostPath
|
$configIdentity = Get-FileIdentity $configPath
|
if ($manifestIdentity.Bytes -ne $pinnedManifestBytes) {
|
throw 'Previous native host manifest bytes mismatch.'
|
}
|
if ($manifestIdentity.Sha256 -cne $pinnedManifestSha256) {
|
throw 'Previous native host manifest SHA-256 mismatch.'
|
}
|
if ($hostIdentity.Bytes -ne $pinnedHostBytes) {
|
throw 'Previous Host executable bytes mismatch.'
|
}
|
if ($hostIdentity.Sha256 -cne $pinnedHostSha256) {
|
throw 'Previous Host executable SHA-256 mismatch.'
|
}
|
if ($configIdentity.Bytes -ne $pinnedConfigBytes) {
|
throw 'Previous config bytes mismatch.'
|
}
|
if ($configIdentity.Sha256 -cne $pinnedConfigSha256) {
|
throw 'Previous config SHA-256 mismatch.'
|
}
|
$previousManifest = Get-StrictJson $manifestPath
|
if ((Compare-Object @('allowed_origins', 'description', 'name', 'path', 'type') @($previousManifest.PSObject.Properties.Name | Sort-Object)) -or
|
$previousManifest.name -cne $expectedHostName -or
|
$previousManifest.path -cne $hostPath -or
|
$previousManifest.type -cne 'stdio' -or
|
@($previousManifest.allowed_origins).Count -ne 1 -or
|
$previousManifest.allowed_origins[0] -cne $expectedOrigin) {
|
throw 'Previous native host manifest identity mismatch.'
|
}
|
$previousConfig = Get-StrictJson $configPath
|
if ($previousConfig.schema -ne 2 -or
|
$previousConfig.required_extension_build -cne $expectedPreviousExtensionBuild -or
|
@($previousConfig.creator_allowlist).Count -ne 1 -or
|
$previousConfig.creator_allowlist[0] -cne '1420210197') {
|
throw 'Previous host config identity mismatch.'
|
}
|
return [pscustomobject]@{
|
Root = $PreviousRoot
|
ManifestPath = $manifestPath
|
HostPath = $hostPath
|
ConfigPath = $configPath
|
}
|
}
|
|
function Get-RegistrySnapshot([string]$Path, [bool]$UseFileProvider) {
|
if (-not (Test-Path -LiteralPath $Path)) {
|
return [pscustomobject]@{
|
Exists = $false
|
ValueNames = @()
|
SubKeyNames = @()
|
DefaultKind = $null
|
DefaultValue = $null
|
}
|
}
|
if ($UseFileProvider) {
|
$item = Get-Item -LiteralPath $Path -Force
|
if (-not $item.PSIsContainer -or ($item.Attributes -band [IO.FileAttributes]::ReparsePoint)) {
|
throw 'Test registry key must be an ordinary non-reparse directory.'
|
}
|
$children = @(Get-ChildItem -LiteralPath $Path -Force)
|
if (@($children | Where-Object { $_.Attributes -band [IO.FileAttributes]::ReparsePoint }).Count -ne 0) {
|
throw 'Test registry contains a reparse path.'
|
}
|
$files = @($children | Where-Object { -not $_.PSIsContainer })
|
$subkeys = @($children | Where-Object { $_.PSIsContainer } | ForEach-Object { $_.Name })
|
$valueNames = @($files | ForEach-Object { if ($_.Name -ceq 'default.value') { '' } else { $_.Name } })
|
$defaultPath = Join-Path $Path 'default.value'
|
$defaultValue = $null
|
$defaultKind = $null
|
if (Test-Path -LiteralPath $defaultPath) {
|
$defaultValue = [System.IO.File]::ReadAllText($defaultPath, [System.Text.UTF8Encoding]::new($false, $true))
|
$defaultKind = 'String'
|
}
|
return [pscustomobject]@{
|
Exists = $true
|
ValueNames = $valueNames
|
SubKeyNames = $subkeys
|
DefaultKind = $defaultKind
|
DefaultValue = $defaultValue
|
}
|
}
|
$key = Get-Item -LiteralPath $Path -Force
|
$valueNames = @($key.GetValueNames())
|
$subKeyNames = @($key.GetSubKeyNames())
|
$defaultValue = $null
|
$defaultKind = $null
|
if ($valueNames -contains '') {
|
$defaultValue = $key.GetValue('', $null, [Microsoft.Win32.RegistryValueOptions]::DoNotExpandEnvironmentNames)
|
$defaultKind = $key.GetValueKind('').ToString()
|
}
|
return [pscustomobject]@{
|
Exists = $true
|
ValueNames = $valueNames
|
SubKeyNames = $subKeyNames
|
DefaultKind = $defaultKind
|
DefaultValue = $defaultValue
|
}
|
}
|
|
function Test-RegistrySnapshotExact([object]$Snapshot, [string]$ExpectedDefault) {
|
return $Snapshot.Exists -and
|
@($Snapshot.ValueNames).Count -eq 1 -and $Snapshot.ValueNames[0] -ceq '' -and
|
@($Snapshot.SubKeyNames).Count -eq 0 -and
|
$Snapshot.DefaultKind -ceq 'String' -and
|
$Snapshot.DefaultValue -ceq $ExpectedDefault
|
}
|
|
function Test-RegistrySnapshotEmpty([object]$Snapshot) {
|
return $Snapshot.Exists -and @($Snapshot.ValueNames).Count -eq 0 -and @($Snapshot.SubKeyNames).Count -eq 0
|
}
|
|
function Set-RegistryDefaultAtomic([string]$Path, [string]$Value, [bool]$UseFileProvider) {
|
if ($UseFileProvider) {
|
$defaultPath = Join-Path $Path 'default.value'
|
if (-not (Test-Path -LiteralPath $defaultPath)) {
|
Write-Utf8CreateNew $defaultPath $Value
|
return
|
}
|
$pending = Join-Path $Path ('.default.pending.' + [Guid]::NewGuid().ToString('N'))
|
$backup = Join-Path $Path ('.default.backup.' + [Guid]::NewGuid().ToString('N'))
|
try {
|
Write-Utf8CreateNew $pending $Value
|
[System.IO.File]::Replace($pending, $defaultPath, $backup, $true)
|
} finally {
|
if (Test-Path -LiteralPath $pending) {
|
Remove-Item -LiteralPath $pending -Force
|
}
|
if (Test-Path -LiteralPath $backup) {
|
Remove-Item -LiteralPath $backup -Force
|
}
|
}
|
return
|
}
|
Set-Item -LiteralPath $Path -Value $Value -ErrorAction Stop
|
}
|
|
function Assert-NewInstallRoot(
|
[string]$InstallRootPath,
|
[string]$InstalledHostPath,
|
[string]$InstalledConfigPath,
|
[string]$InstalledManifestPath,
|
[object]$ExpectedBuildEntry,
|
[object]$ExpectedConfigIdentity
|
) {
|
$rootItem = Get-Item -LiteralPath $InstallRootPath -Force
|
if (-not $rootItem.PSIsContainer -or ($rootItem.Attributes -band [IO.FileAttributes]::ReparsePoint)) {
|
throw 'Installed root must be an ordinary non-reparse directory.'
|
}
|
$children = @(Get-ChildItem -LiteralPath $InstallRootPath -Force)
|
if ($children.Count -ne 3 -or @($children | Where-Object { $_.PSIsContainer }).Count -ne 0 -or
|
(Compare-Object -CaseSensitive @('config.json', 'native-host-manifest.json', $expectedHostExecutable) @($children.Name | Sort-Object))) {
|
throw 'Installed root does not contain the exact three-file set.'
|
}
|
foreach ($child in $children) {
|
if ($child.Attributes -band [IO.FileAttributes]::ReparsePoint) {
|
throw 'Installed root contains a reparse path.'
|
}
|
}
|
$hostIdentity = Get-FileIdentity $InstalledHostPath
|
$configIdentity = Get-FileIdentity $InstalledConfigPath
|
if ($hostIdentity.Bytes -ne $ExpectedBuildEntry.bytes -or $hostIdentity.Sha256 -cne $ExpectedBuildEntry.sha256 -or
|
$configIdentity.Bytes -ne $ExpectedConfigIdentity.Bytes -or $configIdentity.Sha256 -cne $ExpectedConfigIdentity.Sha256) {
|
throw 'Installed payload identity mismatch.'
|
}
|
$persistedManifest = Get-StrictJson $InstalledManifestPath
|
if ((Compare-Object @('allowed_origins', 'description', 'name', 'path', 'type') @($persistedManifest.PSObject.Properties.Name | Sort-Object)) -or
|
$persistedManifest.name -cne $expectedHostName -or
|
$persistedManifest.path -cne $InstalledHostPath -or
|
$persistedManifest.type -cne 'stdio' -or
|
@($persistedManifest.allowed_origins).Count -ne 1 -or
|
$persistedManifest.allowed_origins[0] -cne $expectedOrigin) {
|
throw 'Installed native host manifest identity mismatch.'
|
}
|
}
|
|
function Remove-ProvenOwnedInstallRoot(
|
[string]$InstallRootPath,
|
[string]$InstalledHostPath,
|
[string]$InstalledConfigPath,
|
[string]$InstalledManifestPath,
|
[object]$ExpectedBuildEntry,
|
[object]$ExpectedConfigIdentity
|
) {
|
if (-not (Test-Path -LiteralPath $InstallRootPath)) {
|
return
|
}
|
$rootItem = Get-Item -LiteralPath $InstallRootPath -Force
|
if (-not $rootItem.PSIsContainer -or ($rootItem.Attributes -band [IO.FileAttributes]::ReparsePoint)) {
|
throw 'RECOVERY_REQUIRED: target root is not a proven owned ordinary directory.'
|
}
|
$children = @(Get-ChildItem -LiteralPath $InstallRootPath -Force)
|
if (@($children | Where-Object { $_.PSIsContainer -or ($_.Attributes -band [IO.FileAttributes]::ReparsePoint) }).Count -ne 0 -or
|
@($children | Where-Object { $_.Name -cnotin @('config.json', 'native-host-manifest.json', $expectedHostExecutable) }).Count -ne 0) {
|
throw 'RECOVERY_REQUIRED: target root contains an unknown or reparse object.'
|
}
|
if (Test-Path -LiteralPath $InstalledHostPath) {
|
$identity = Get-FileIdentity $InstalledHostPath
|
if ($identity.Bytes -ne $ExpectedBuildEntry.bytes -or $identity.Sha256 -cne $ExpectedBuildEntry.sha256) {
|
throw 'RECOVERY_REQUIRED: target host identity is ambiguous.'
|
}
|
}
|
if (Test-Path -LiteralPath $InstalledConfigPath) {
|
$identity = Get-FileIdentity $InstalledConfigPath
|
if ($identity.Bytes -ne $ExpectedConfigIdentity.Bytes -or $identity.Sha256 -cne $ExpectedConfigIdentity.Sha256) {
|
throw 'RECOVERY_REQUIRED: target config identity is ambiguous.'
|
}
|
}
|
if (Test-Path -LiteralPath $InstalledManifestPath) {
|
$persistedManifest = Get-StrictJson $InstalledManifestPath
|
if ($persistedManifest.name -cne $expectedHostName -or
|
$persistedManifest.path -cne $InstalledHostPath -or
|
$persistedManifest.type -cne 'stdio' -or
|
@($persistedManifest.allowed_origins).Count -ne 1 -or
|
$persistedManifest.allowed_origins[0] -cne $expectedOrigin) {
|
throw 'RECOVERY_REQUIRED: target manifest identity is ambiguous.'
|
}
|
}
|
Remove-Item -LiteralPath $InstallRootPath -Recurse -Force
|
}
|
|
if ($ObservedExtensionId -cne $expectedId) {
|
throw 'Observed Chrome extension ID does not match the pinned identity.'
|
}
|
$der = [Convert]::FromBase64String($publicKey)
|
$sha = [System.Security.Cryptography.SHA256]::Create()
|
try {
|
$publicDigest = $sha.ComputeHash($der)
|
$derHash = -join ($publicDigest | ForEach-Object { $_.ToString('X2') })
|
} finally {
|
$sha.Dispose()
|
}
|
if ($derHash -cne $expectedPublicDerHash) {
|
throw 'Pinned extension public key hash does not match.'
|
}
|
$alphabet = 'abcdefghijklmnop'
|
$idBuilder = [System.Text.StringBuilder]::new()
|
foreach ($value in $publicDigest[0..15]) {
|
$null = $idBuilder.Append($alphabet[$value -shr 4])
|
$null = $idBuilder.Append($alphabet[$value -band 15])
|
}
|
if ($idBuilder.ToString() -cne $expectedId) {
|
throw 'Pinned public key does not recompute to the expected extension ID.'
|
}
|
|
$sourceRoot = (Resolve-Path -LiteralPath (Split-Path -Parent $PSCommandPath)).Path
|
$expectedSourceManifestPath = Join-Path $sourceRoot 'source-artifact-manifest.json'
|
$sourceManifestPath = (Resolve-Path -LiteralPath $SourceArtifactManifest).Path
|
if ($sourceManifestPath -cne $expectedSourceManifestPath) {
|
throw 'Only the bundled reviewed source artifact manifest is accepted.'
|
}
|
$buildManifestPath = (Resolve-Path -LiteralPath $BuildArtifactManifest).Path
|
if ([System.IO.Path]::GetFileName($buildManifestPath) -cne 'build-artifact-manifest.json') {
|
throw 'Build artifact manifest name mismatch.'
|
}
|
$buildRoot = Split-Path -Parent $buildManifestPath
|
$resolvedHost = (Resolve-Path -LiteralPath $HostExecutable).Path
|
|
$sourceApproval = Get-ExternalReceipt $ApprovedSourceReceipt @($sourceRoot, $buildRoot)
|
$sourceReceipt = $sourceApproval.Value
|
$expectedSourceReceiptKeys = @(
|
'schema', 'task_id', 'approval_scope', 'approved_by_role', 'status',
|
'source_artifact_manifest_bytes', 'source_artifact_manifest_sha256'
|
)
|
if ((Compare-Object ($expectedSourceReceiptKeys | Sort-Object) @($sourceReceipt.PSObject.Properties.Name | Sort-Object)) -or
|
$sourceReceipt.schema -ne 1 -or
|
$sourceReceipt.task_id -cne 'DEV-PROJECT-INFO-BILI-AUTHENTICATED-SESSION-DOWNLOAD-20260805-001' -or
|
$sourceReceipt.approval_scope -cne 'controlled-build-source-manifest' -or
|
$sourceReceipt.approved_by_role -notin @('dev.reviewer.project', 'project.admin') -or
|
$sourceReceipt.status -cne 'APPROVED' -or
|
$sourceReceipt.source_artifact_manifest_sha256 -notmatch '^[A-F0-9]{64}$' -or
|
$sourceReceipt.source_artifact_manifest_bytes -ne (Get-Item -LiteralPath $sourceManifestPath).Length -or
|
$sourceReceipt.source_artifact_manifest_sha256 -cne (Get-FileHash -Algorithm SHA256 -LiteralPath $sourceManifestPath).Hash) {
|
throw 'External approved source receipt does not match the source manifest.'
|
}
|
|
$buildApproval = Get-ExternalReceipt $ApprovedBuildReceipt @($sourceRoot, $buildRoot)
|
$buildReceipt = $buildApproval.Value
|
$expectedBuildReceiptKeys = @(
|
'schema', 'task_id', 'approval_scope', 'approved_by_role', 'status',
|
'source_artifact_manifest_sha256', 'build_artifact_manifest_bytes',
|
'build_artifact_manifest_sha256', 'host_executable_bytes', 'host_executable_sha256'
|
)
|
if ((Compare-Object ($expectedBuildReceiptKeys | Sort-Object) @($buildReceipt.PSObject.Properties.Name | Sort-Object)) -or
|
$buildReceipt.schema -ne 1 -or
|
$buildReceipt.task_id -cne 'DEV-PROJECT-INFO-BILI-AUTHENTICATED-SESSION-DOWNLOAD-20260805-001' -or
|
$buildReceipt.approval_scope -cne 'install-exact-build' -or
|
$buildReceipt.approved_by_role -notin @('dev.reviewer.project', 'project.admin') -or
|
$buildReceipt.status -cne 'APPROVED' -or
|
$buildReceipt.source_artifact_manifest_sha256 -cne $sourceReceipt.source_artifact_manifest_sha256 -or
|
$buildReceipt.build_artifact_manifest_sha256 -notmatch '^[A-F0-9]{64}$' -or
|
$buildReceipt.host_executable_sha256 -notmatch '^[A-F0-9]{64}$' -or
|
$buildReceipt.build_artifact_manifest_bytes -ne (Get-Item -LiteralPath $buildManifestPath).Length -or
|
$buildReceipt.build_artifact_manifest_sha256 -cne (Get-FileHash -Algorithm SHA256 -LiteralPath $buildManifestPath).Hash -or
|
$buildReceipt.host_executable_bytes -ne (Get-Item -LiteralPath $resolvedHost).Length -or
|
$buildReceipt.host_executable_sha256 -cne (Get-FileHash -Algorithm SHA256 -LiteralPath $resolvedHost).Hash) {
|
throw 'External approved build receipt does not match the exact build artifacts.'
|
}
|
|
$sourceManifest = Get-StrictJson $sourceManifestPath
|
$expectedSourceFiles = @(
|
'__init__.py', 'background.js', 'build_host.ps1', 'config.example.json',
|
'constants.py', 'dependencies/dependency-artifact-manifest.json',
|
'dependencies/yt_dlp-2026.7.4-py3-none-any.whl',
|
'formal_legacy_identity_manifest.py', 'install_native_host.ps1', 'job.py', 'manifest.json',
|
'native-host-manifest.template.json', 'native_host.py', 'protocol.py',
|
'queue-producer.example.json', 'queue_producer.py', 'queue_state.py',
|
'sidepanel.css', 'sidepanel.html', 'sidepanel.js', 'worker.py'
|
)
|
Assert-ExactSourceSnapshot $sourceRoot $sourceManifestPath $expectedSourceFiles $sourceManifest
|
$dependencyManifestPath = Join-Path $sourceRoot 'dependencies/dependency-artifact-manifest.json'
|
$dependencyManifestItem = Get-Item -LiteralPath $dependencyManifestPath
|
if ($sourceManifest.schema -ne 1 -or $sourceManifest.scope -cne 'generic-bilibili-queue' -or
|
$sourceManifest.extension_id -cne $expectedId -or
|
$sourceManifest.extension_build -cne $expectedExtensionBuild -or
|
$sourceManifest.host_build -cne $expectedHostBuild -or
|
$sourceManifest.dependency_artifact_manifest_bytes -ne $dependencyManifestItem.Length -or
|
$sourceManifest.dependency_artifact_manifest_sha256 -cne (Get-FileHash -Algorithm SHA256 -LiteralPath $dependencyManifestPath).Hash -or
|
(Compare-Object -CaseSensitive ($expectedSourceFiles | Sort-Object) @($sourceManifest.files | ForEach-Object { $_.path } | Sort-Object))) {
|
throw 'Source artifact manifest identity or file set mismatch.'
|
}
|
$metadataContract = $sourceManifest.archive_metadata_contract
|
$expectedMetadataContractKeys = @(
|
'schema', 'root', 'relative_files', 'distribution_name', 'distribution_version',
|
'allowed_type_codes', 'source_date_epoch', 'tree_hash_algorithm', 'canonical_tree_sha256'
|
)
|
if ($null -eq $metadataContract -or
|
(Compare-Object ($expectedMetadataContractKeys | Sort-Object) @($metadataContract.PSObject.Properties.Name | Sort-Object)) -or
|
$metadataContract.schema -ne 1 -or
|
$metadataContract.root -cne 'yt_dlp-2026.7.4.dist-info' -or
|
(Compare-Object -CaseSensitive @(
|
'INSTALLER', 'METADATA', 'RECORD', 'REQUESTED', 'WHEEL',
|
'entry_points.txt', 'licenses/LICENSE'
|
) @($metadataContract.relative_files)) -or
|
$metadataContract.distribution_name -cne 'yt-dlp' -or
|
$metadataContract.distribution_version -cne '2026.7.4' -or
|
(Compare-Object -CaseSensitive @('b', 'x') @($metadataContract.allowed_type_codes)) -or
|
$metadataContract.source_date_epoch -ne 1786207924 -or
|
$metadataContract.tree_hash_algorithm -cne 'sha256(path-utf8,nul,decimal-bytes-ascii,nul,payload-sha256-lower-hex-ascii,lf)-upper-hex-v1' -or
|
$metadataContract.canonical_tree_sha256 -notmatch '^[A-F0-9]{64}$') {
|
throw 'Source artifact metadata contract mismatch.'
|
}
|
$seenSourcePaths = [System.Collections.Generic.HashSet[string]]::new([StringComparer]::Ordinal)
|
foreach ($entry in $sourceManifest.files) {
|
if ($entry.path -notmatch '^[A-Za-z0-9._/-]+$' -or $entry.path.Contains('..') -or
|
$entry.sha256 -notmatch '^[A-F0-9]{64}$' -or $entry.bytes -lt 1 -or
|
-not $seenSourcePaths.Add($entry.path)) {
|
throw 'Source artifact manifest contains an invalid entry.'
|
}
|
$candidate = [System.IO.Path]::GetFullPath((Join-Path $sourceRoot $entry.path))
|
if (-not $candidate.StartsWith($sourceRoot + [System.IO.Path]::DirectorySeparatorChar, [StringComparison]::OrdinalIgnoreCase)) {
|
throw 'Source artifact path escaped its root.'
|
}
|
$item = Get-Item -LiteralPath $candidate
|
if (($item.Attributes -band [IO.FileAttributes]::ReparsePoint) -or
|
$item.Length -ne $entry.bytes -or
|
(Get-FileHash -Algorithm SHA256 -LiteralPath $candidate).Hash -cne $entry.sha256) {
|
throw 'Source artifact hash mismatch.'
|
}
|
}
|
|
$buildManifest = Get-StrictJson $buildManifestPath
|
$buildScript = Join-Path $sourceRoot 'build_host.ps1'
|
if ($buildManifest.schema -ne 2 -or $buildManifest.scope -cne 'generic-bilibili-queue' -or
|
$buildManifest.extension_id -cne $expectedId -or
|
$buildManifest.extension_build -cne $expectedExtensionBuild -or
|
$buildManifest.host_build -cne $expectedHostBuild -or
|
$buildManifest.packaging -cne 'pyinstaller-onefile' -or
|
$buildManifest.pyinstaller_version -cne '6.15.0' -or
|
$buildManifest.yt_dlp_version -cne '2026.7.4' -or
|
$buildManifest.pyinstaller_executable_bytes -ne 108469 -or
|
$buildManifest.pyinstaller_executable_sha256 -cne 'D5DC4427C5E5D417457DAE6FD8B50EF2AFA0A4CE5FDFD5767AB20C5B56555C39' -or
|
$buildManifest.builder_provision_receipt_bytes -ne 2027 -or
|
$buildManifest.builder_provision_receipt_sha256 -cne 'B65F4184E8782394F2CC27C47CB8656C942E366FD80E8FA7A548CE5A3367BACF' -or
|
$buildManifest.build_script_sha256 -cne (Get-FileHash -Algorithm SHA256 -LiteralPath $buildScript).Hash -or
|
$buildManifest.source_artifact_manifest_bytes -ne (Get-Item -LiteralPath $sourceManifestPath).Length -or
|
$buildManifest.source_artifact_manifest_sha256 -cne (Get-FileHash -Algorithm SHA256 -LiteralPath $sourceManifestPath).Hash -or
|
$buildManifest.dependency_artifact_manifest_bytes -ne $dependencyManifestItem.Length -or
|
$buildManifest.dependency_artifact_manifest_sha256 -cne (Get-FileHash -Algorithm SHA256 -LiteralPath $dependencyManifestPath).Hash -or
|
$buildManifest.yt_dlp_wheel_sha256 -cne 'F11F2B11D5A8AC4059F9BDF29FA4407DC7C6BB00C5097E95CA22A7A9DB518266' -or
|
$buildManifest.archive_verification.status -cne 'PASS' -or
|
$buildManifest.archive_verification.method -cne 'PyInstaller 6.15.0 CArchiveReader exact metadata bytes' -or
|
$buildManifest.archive_verification.metadata_entry -cne ($metadataContract.root + '/METADATA') -or
|
$buildManifest.archive_verification.metadata_files -ne @($metadataContract.relative_files).Count -or
|
$null -eq $buildManifest.archive_verification.metadata_type_codes -or
|
@($buildManifest.archive_verification.metadata_type_codes).Count -lt 1 -or
|
@($buildManifest.archive_verification.metadata_type_codes | Where-Object { $_ -cnotin @($metadataContract.allowed_type_codes) }).Count -ne 0 -or
|
$buildManifest.archive_verification.metadata_tree_sha256 -cne $metadataContract.canonical_tree_sha256 -or
|
(Compare-Object @(
|
'bili_authenticated_extension.worker', 'yt_dlp', 'yt_dlp.downloader',
|
'yt_dlp.globals', 'yt_dlp.plugins', 'yt_dlp.version'
|
) @($buildManifest.archive_verification.required_modules)) -or
|
$buildManifest.builder_python_sha256 -notmatch '^[A-F0-9]{64}$' -or
|
$null -eq $buildManifest.files -or $buildManifest.files.Count -ne 1) {
|
throw 'Build artifact receipt identity mismatch.'
|
}
|
$buildEntry = $buildManifest.files[0]
|
if ($buildEntry.path -cne $expectedHostExecutable -or
|
$buildEntry.sha256 -notmatch '^[A-F0-9]{64}$' -or $buildEntry.bytes -lt 1) {
|
throw 'Build artifact receipt file set mismatch.'
|
}
|
$expectedBuiltHost = [System.IO.Path]::GetFullPath((Join-Path $buildRoot $buildEntry.path))
|
if ($resolvedHost -cne $expectedBuiltHost) {
|
throw 'Host executable is not the exact build receipt payload.'
|
}
|
$hostItem = Get-Item -LiteralPath $resolvedHost
|
if (($hostItem.Attributes -band [IO.FileAttributes]::ReparsePoint) -or
|
$hostItem.Length -ne $buildEntry.bytes -or
|
(Get-FileHash -Algorithm SHA256 -LiteralPath $resolvedHost).Hash -cne $buildEntry.sha256) {
|
throw 'Host executable does not match the build artifact receipt.'
|
}
|
$unexpectedBuildFiles = @(Get-ChildItem -LiteralPath $buildRoot -Force | Where-Object { $_.Name -notin @($expectedHostExecutable, 'build-artifact-manifest.json') })
|
if ($unexpectedBuildFiles.Count -ne 0) {
|
throw 'Build root contains an unreviewed dependency or file.'
|
}
|
|
$config = (Resolve-Path -LiteralPath $ConfigFile).Path
|
$root = [System.IO.Path]::GetFullPath($InstallRoot)
|
if (-not [System.IO.Path]::IsPathRooted($root) -or $root.StartsWith('\\')) {
|
throw 'InstallRoot must be an absolute local path.'
|
}
|
$configItem = Get-Item -LiteralPath $config
|
if (($configItem.Attributes -band [IO.FileAttributes]::ReparsePoint) -or $configItem.PSIsContainer) {
|
throw 'Config file must be a regular non-reparse file.'
|
}
|
$configIdentity = Get-FileIdentity $config
|
$configObject = Get-StrictJson $config
|
$expectedConfigKeys = @(
|
'schema', 'creator_allowlist', 'queue_path', 'queue_state_path', 'queue_lock_path',
|
'reload_state_path', 'reload_generation', 'required_extension_build',
|
'ffmpeg', 'ffmpeg_sha256', 'ffprobe', 'ffprobe_sha256', 'bridge_python',
|
'bridge_python_sha256', 'bridge_script', 'bridge_script_sha256',
|
'yt_dlp_executable', 'yt_dlp_executable_sha256', 'destination',
|
'creator_name', 'formal_manifest_path', 'processing_handoff_path'
|
)
|
$actualConfigKeys = @($configObject.PSObject.Properties.Name | Sort-Object)
|
if ((Compare-Object ($expectedConfigKeys | Sort-Object) $actualConfigKeys) -or
|
$configObject.schema -ne 2 -or
|
$configObject.required_extension_build -cne $expectedExtensionBuild -or
|
$null -eq $configObject.creator_allowlist -or @($configObject.creator_allowlist).Count -lt 1 -or
|
$configObject.reload_generation -cne 'bili-auth-generic-v027' -or
|
$configObject.bridge_script_sha256 -cne '00F11DAF8387160DB863C89F0B33AB8480422233FF42199189222C989C7ED07E') {
|
throw 'Host config identity mismatch.'
|
}
|
foreach ($name in @('ffmpeg', 'ffprobe', 'bridge_python', 'bridge_script', 'yt_dlp_executable')) {
|
$value = $configObject.$name
|
$expectedHash = $configObject."${name}_sha256"
|
if (-not [System.IO.Path]::IsPathRooted($value) -or $value.StartsWith('\\') -or
|
$expectedHash -notmatch '^[A-F0-9]{64}$') {
|
throw 'Host config contains an unsafe pinned file.'
|
}
|
$resolvedValue = (Resolve-Path -LiteralPath $value).Path
|
$valueItem = Get-Item -LiteralPath $resolvedValue
|
if (($valueItem.Attributes -band [IO.FileAttributes]::ReparsePoint) -or
|
(Get-FileHash -Algorithm SHA256 -LiteralPath $resolvedValue).Hash -cne $expectedHash) {
|
throw 'Host config pinned file hash mismatch.'
|
}
|
}
|
if (-not [System.IO.Path]::IsPathRooted($configObject.destination) -or $configObject.destination.StartsWith('\\')) {
|
throw 'Host config destination must be an absolute local path.'
|
}
|
$resolvedDestination = (Resolve-Path -LiteralPath $configObject.destination).Path
|
$destinationItem = Get-Item -LiteralPath $resolvedDestination
|
if (-not $destinationItem.PSIsContainer -or ($destinationItem.Attributes -band [IO.FileAttributes]::ReparsePoint)) {
|
throw 'Host config destination must be an existing non-reparse directory.'
|
}
|
if ([string]::IsNullOrWhiteSpace([string]$configObject.creator_name) -or
|
[Text.Encoding]::UTF8.GetByteCount([string]$configObject.creator_name) -gt 240) {
|
throw 'Host config creator_name is invalid.'
|
}
|
foreach ($name in @('formal_manifest_path', 'processing_handoff_path')) {
|
$value = [string]$configObject.$name
|
if (-not [System.IO.Path]::IsPathRooted($value) -or $value.StartsWith('\\')) {
|
throw 'Host governed output path must be absolute and local.'
|
}
|
$parent = [System.IO.Path]::GetDirectoryName($value)
|
$parentItem = Get-Item -LiteralPath $parent
|
if (-not $parentItem.PSIsContainer -or ($parentItem.Attributes -band [IO.FileAttributes]::ReparsePoint)) {
|
throw 'Host governed output parent must be an ordinary directory.'
|
}
|
if (Test-Path -LiteralPath $value) {
|
$item = Get-Item -LiteralPath $value
|
if ($item.PSIsContainer -or ($item.Attributes -band [IO.FileAttributes]::ReparsePoint)) {
|
throw 'Host governed output must be an ordinary file.'
|
}
|
} elseif ($name -ceq 'formal_manifest_path') {
|
throw 'Host formal manifest must already exist.'
|
}
|
}
|
if ($configObject.formal_manifest_path -ceq $configObject.processing_handoff_path) {
|
throw 'Host governed output paths must be distinct.'
|
}
|
if (Test-Path -LiteralPath $root) {
|
throw 'InstallRoot already exists; overwrite is forbidden.'
|
}
|
|
$registryPath = "HKCU:\Software\Google\Chrome\NativeMessagingHosts\$expectedHostName"
|
$previousRoot = [System.IO.Path]::GetFullPath((Join-Path $env:LOCALAPPDATA "project-info\bili-auth-native-host\$expectedPreviousVersion"))
|
if ($TestFileRegistryProvider) {
|
if (-not $TestRegistryRoot -or -not $TestPreviousInstallRoot -or -not $TestPreviousArtifactReceipt) {
|
throw 'The test file registry provider requires its registry root and previous-install fixture inputs.'
|
}
|
$testRegistryBase = [System.IO.Path]::GetFullPath($TestRegistryRoot)
|
$tempBase = [System.IO.Path]::GetFullPath([System.IO.Path]::GetTempPath())
|
$previousRoot = [System.IO.Path]::GetFullPath($TestPreviousInstallRoot)
|
$previousReceiptPath = (Resolve-Path -LiteralPath $TestPreviousArtifactReceipt).Path
|
if (-not $testRegistryBase.StartsWith($tempBase, [StringComparison]::OrdinalIgnoreCase) -or
|
-not $previousRoot.StartsWith($tempBase, [StringComparison]::OrdinalIgnoreCase) -or
|
-not $previousReceiptPath.StartsWith($tempBase, [StringComparison]::OrdinalIgnoreCase) -or
|
($root -notlike ($tempBase.TrimEnd('\') + '\*'))) {
|
throw 'Test-only installer paths must be under the current temporary directory.'
|
}
|
$previousReceiptItem = Get-Item -LiteralPath $previousReceiptPath -Force
|
if ($previousReceiptItem.PSIsContainer -or ($previousReceiptItem.Attributes -band [IO.FileAttributes]::ReparsePoint)) {
|
throw 'Test previous-artifact receipt must be a regular non-reparse file.'
|
}
|
$previousReceipt = Get-StrictJson $previousReceiptPath
|
$expectedPreviousReceiptKeys = @(
|
'schema', 'manifest_bytes', 'manifest_sha256', 'host_executable_bytes',
|
'host_executable_sha256', 'config_bytes', 'config_sha256'
|
)
|
if ((Compare-Object ($expectedPreviousReceiptKeys | Sort-Object) @($previousReceipt.PSObject.Properties.Name | Sort-Object)) -or
|
$previousReceipt.schema -ne 1) {
|
throw 'Test previous-artifact receipt identity mismatch.'
|
}
|
$expectedPreviousManifestBytes = $previousReceipt.manifest_bytes
|
$expectedPreviousManifestSha256 = $previousReceipt.manifest_sha256
|
$expectedPreviousHostBytes = $previousReceipt.host_executable_bytes
|
$expectedPreviousHostSha256 = $previousReceipt.host_executable_sha256
|
$expectedPreviousConfigBytes = $previousReceipt.config_bytes
|
$expectedPreviousConfigSha256 = $previousReceipt.config_sha256
|
$registryPath = Join-Path $testRegistryBase $expectedHostName
|
} elseif ($TestRegistryRoot -or $TestPreviousInstallRoot -or $TestPreviousArtifactReceipt -or $InjectFailure -cne 'none') {
|
throw 'Test-only controls require TestFileRegistryProvider.'
|
} else {
|
$expectedInstallRoot = [System.IO.Path]::GetFullPath((Join-Path $env:LOCALAPPDATA 'project-info\bili-auth-native-host\1.2.25+20260829.generic.v027'))
|
if ($root -cne $expectedInstallRoot) {
|
throw 'Production InstallRoot does not match the pinned v027 location.'
|
}
|
}
|
|
$previousIdentityParameters = @{
|
PreviousRoot = $previousRoot
|
ManifestBytes = $expectedPreviousManifestBytes
|
ManifestSha256 = $expectedPreviousManifestSha256
|
HostBytes = $expectedPreviousHostBytes
|
HostSha256 = $expectedPreviousHostSha256
|
ConfigBytes = $expectedPreviousConfigBytes
|
ConfigSha256 = $expectedPreviousConfigSha256
|
}
|
|
$registryPreimage = Get-RegistrySnapshot $registryPath $TestFileRegistryProvider.IsPresent
|
if (-not $registryPreimage.Exists) {
|
throw 'Exact v026 Native Messaging registration preimage is required.'
|
}
|
$previousInstall = Assert-ExactPreviousInstall @previousIdentityParameters
|
if (-not (Test-RegistrySnapshotExact $registryPreimage $previousInstall.ManifestPath)) {
|
throw 'Existing Native Messaging registration does not match the pinned v026 preimage.'
|
}
|
$installMode = 'EXACT_V026_TO_V027_SWITCH'
|
|
if (-not $Install) {
|
[pscustomobject]@{
|
result = 'VALIDATION_PASS_ONLY'
|
extension_id = $expectedId
|
origin = $expectedOrigin
|
host_name = $expectedHostName
|
packaging = 'pyinstaller-onefile'
|
install_mode = $installMode
|
} | ConvertTo-Json -Compress
|
return
|
}
|
|
if ($PSCmdlet.ShouldProcess($root, 'Install exact-BVID Native Messaging host for current user')) {
|
$installedHost = Join-Path $root $expectedHostExecutable
|
$installedConfig = Join-Path $root 'config.json'
|
$manifestPath = Join-Path $root 'native-host-manifest.json'
|
try {
|
[System.IO.Directory]::CreateDirectory($root) | Out-Null
|
Stop-Injected 'after-root'
|
Copy-CreateNew $resolvedHost $installedHost
|
if ((Get-Item -LiteralPath $installedHost).Length -ne $buildEntry.bytes -or
|
(Get-FileHash -Algorithm SHA256 -LiteralPath $installedHost).Hash -cne $buildEntry.sha256) {
|
throw 'Installed host reread verification failed.'
|
}
|
Stop-Injected 'after-payload'
|
Copy-CreateNew $config $installedConfig
|
if ((Get-Item -LiteralPath $installedConfig).Length -ne $configItem.Length -or
|
(Get-FileHash -Algorithm SHA256 -LiteralPath $installedConfig).Hash -cne (Get-FileHash -Algorithm SHA256 -LiteralPath $config).Hash) {
|
throw 'Installed config reread verification failed.'
|
}
|
Stop-Injected 'after-config'
|
$manifest = [ordered]@{
|
name = $expectedHostName
|
description = 'project-info exact-BVID authenticated ingress'
|
path = $installedHost
|
type = 'stdio'
|
allowed_origins = @($expectedOrigin)
|
}
|
Write-Utf8CreateNew $manifestPath ($manifest | ConvertTo-Json -Depth 3)
|
Assert-NewInstallRoot $root $installedHost $installedConfig $manifestPath $buildEntry $configIdentity
|
Stop-Injected 'after-manifest'
|
|
$lastRegistryPreimage = Get-RegistrySnapshot $registryPath $TestFileRegistryProvider.IsPresent
|
$null = Assert-ExactPreviousInstall @previousIdentityParameters
|
if (-not (Test-RegistrySnapshotExact $lastRegistryPreimage $previousInstall.ManifestPath)) {
|
throw 'Native Messaging v026 preimage changed before the registry switch.'
|
}
|
Stop-Injected 'after-registry-key'
|
Set-RegistryDefaultAtomic $registryPath $manifestPath $TestFileRegistryProvider.IsPresent
|
if ($InjectFailure -ceq 'after-registry-value-mixed') {
|
Write-Utf8CreateNew (Join-Path $registryPath 'unexpected.value') 'ambiguous'
|
throw 'Injected ambiguous registry state after v026 switch.'
|
}
|
Stop-Injected 'after-registry-value'
|
$committedRegistry = Get-RegistrySnapshot $registryPath $TestFileRegistryProvider.IsPresent
|
if (-not (Test-RegistrySnapshotExact $committedRegistry $manifestPath)) {
|
throw 'Native Messaging v026 registry commit reread verification failed.'
|
}
|
Assert-NewInstallRoot $root $installedHost $installedConfig $manifestPath $buildEntry $configIdentity
|
} catch {
|
$originalFailure = $_.Exception
|
try {
|
$rollbackRegistry = Get-RegistrySnapshot $registryPath $TestFileRegistryProvider.IsPresent
|
if (Test-RegistrySnapshotExact $rollbackRegistry $manifestPath) {
|
Set-RegistryDefaultAtomic $registryPath $previousInstall.ManifestPath $TestFileRegistryProvider.IsPresent
|
} elseif (-not (Test-RegistrySnapshotExact $rollbackRegistry $previousInstall.ManifestPath)) {
|
throw 'RECOVERY_REQUIRED: Native Messaging registration is neither the pinned v026 preimage nor this v027 attempt.'
|
}
|
$restoredRegistry = Get-RegistrySnapshot $registryPath $TestFileRegistryProvider.IsPresent
|
if (-not (Test-RegistrySnapshotExact $restoredRegistry $previousInstall.ManifestPath)) {
|
throw 'RECOVERY_REQUIRED: Native Messaging v026 registry preimage was not restored.'
|
}
|
$null = Assert-ExactPreviousInstall @previousIdentityParameters
|
Remove-ProvenOwnedInstallRoot $root $installedHost $installedConfig $manifestPath $buildEntry $configIdentity
|
} catch {
|
throw "RECOVERY_REQUIRED: $($_.Exception.Message) Original failure: $($originalFailure.Message)"
|
}
|
throw $originalFailure
|
}
|
}
|