| | |
| | | [Parameter(DontShow = $true)] |
| | | [string]$TestRegistryRoot, |
| | | [Parameter(DontShow = $true)] |
| | | [ValidateSet('none', 'after-root', 'after-payload', 'after-config', 'after-manifest', 'after-registry-key', 'after-registry-value')] |
| | | [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' |
| | | ) |
| | | |
| | |
| | | $expectedOrigin = 'chrome-extension://oidmclckpdmpabbfedplkbdplmfcenbb/' |
| | | $expectedHostName = 'com.project_info.bili_auth_ingress' |
| | | $expectedPublicDerHash = 'E83C2B2AF3CF011543FBA13FBC524D1122EEA68548F9F27B9F7A82B5D594666C' |
| | | $expectedExtensionBuild = 'project-info-bili-auth-ingress/1.0.0+20260805.v002' |
| | | $expectedHostBuild = 'project-info-bili-auth-native-host/1.0.0+20260805.v002' |
| | | $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) { |
| | |
| | | } |
| | | } |
| | | |
| | | 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 { |
| | |
| | | } 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) { |
| | |
| | | '__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', |
| | | 'install_native_host.ps1', 'job.py', 'manifest.json', |
| | | '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-ExactSourceTree $sourceRoot $sourceManifestPath $expectedSourceFiles |
| | | 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.target -cne 'BV1HA3o6oEJJ' -or |
| | | 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 |
| | |
| | | |
| | | $buildManifest = Get-StrictJson $buildManifestPath |
| | | $buildScript = Join-Path $sourceRoot 'build_host.ps1' |
| | | if ($buildManifest.schema -ne 2 -or $buildManifest.target -cne 'BV1HA3o6oEJJ' -or |
| | | 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 |
| | |
| | | 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', 'target', 'canonical_url', 'ffmpeg', 'ffmpeg_sha256', 'ffprobe', |
| | | 'ffprobe_sha256', 'bridge_python', 'bridge_python_sha256', 'bridge_script', |
| | | 'bridge_script_sha256', 'batch_json', 'batch_json_sha256', 'yt_dlp_executable', |
| | | 'yt_dlp_executable_sha256', 'destination' |
| | | '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 1 -or $configObject.target -cne 'BV1HA3o6oEJJ' -or |
| | | $configObject.canonical_url -cne 'https://www.bilibili.com/video/BV1HA3o6oEJJ' -or |
| | | $configObject.bridge_script_sha256 -cne '749FC486B0F42315BD463F11771FE2A7C71CAB53DD9AC2E411CE82E1175DFF13') { |
| | | $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', 'batch_json', 'yt_dlp_executable')) { |
| | | 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 |
| | |
| | | if (-not $destinationItem.PSIsContainer -or ($destinationItem.Attributes -band [IO.FileAttributes]::ReparsePoint)) { |
| | | throw 'Host config destination must be an existing non-reparse directory.' |
| | | } |
| | | if (Get-ChildItem -LiteralPath $resolvedDestination -File | Where-Object { $_.Name.StartsWith('BV1HA3o6oEJJ.', [StringComparison]::OrdinalIgnoreCase) }) { |
| | | throw 'Formal output already exists; overwrite is forbidden.' |
| | | 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) { |
| | | throw 'TestRegistryRoot is required for the test file registry provider.' |
| | | 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()) |
| | | if (-not $testRegistryBase.StartsWith($tempBase, [StringComparison]::OrdinalIgnoreCase)) { |
| | | throw 'The test registry provider must be under the current temporary directory.' |
| | | $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 $InjectFailure -cne 'none') { |
| | | } 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.' |
| | | } |
| | | if (Test-Path -LiteralPath $registryPath) { |
| | | throw 'Native Messaging registration already exists; overwrite is forbidden.' |
| | | } |
| | | |
| | | $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]@{ |
| | |
| | | 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' |
| | | $installedHost = Join-Path $root $expectedHostExecutable |
| | | $installedConfig = Join-Path $root 'config.json' |
| | | 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 config reread verification failed.' |
| | | } |
| | | Stop-Injected 'after-config' |
| | | $manifestPath = Join-Path $root 'native-host-manifest.json' |
| | | $manifest = [ordered]@{ |
| | | name = $expectedHostName |
| | | description = 'project-info exact-BVID authenticated ingress' |
| | |
| | | allowed_origins = @($expectedOrigin) |
| | | } |
| | | Write-Utf8CreateNew $manifestPath ($manifest | ConvertTo-Json -Depth 3) |
| | | $persistedManifest = Get-StrictJson $manifestPath |
| | | if ($persistedManifest.name -cne $expectedHostName -or |
| | | $persistedManifest.path -cne $installedHost -or |
| | | $persistedManifest.type -cne 'stdio' -or |
| | | $persistedManifest.allowed_origins.Count -ne 1 -or |
| | | $persistedManifest.allowed_origins[0] -cne $expectedOrigin) { |
| | | throw 'Native host manifest reread verification failed.' |
| | | } |
| | | Assert-NewInstallRoot $root $installedHost $installedConfig $manifestPath $buildEntry $configIdentity |
| | | Stop-Injected 'after-manifest' |
| | | |
| | | if ($TestFileRegistryProvider) { |
| | | [System.IO.Directory]::CreateDirectory($registryPath) | Out-Null |
| | | } else { |
| | | New-Item -Path $registryPath -ErrorAction Stop | Out-Null |
| | | $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' |
| | | if ($TestFileRegistryProvider) { |
| | | Write-Utf8CreateNew (Join-Path $registryPath 'default.value') $manifestPath |
| | | } else { |
| | | Set-Item -LiteralPath $registryPath -Value $manifestPath -ErrorAction Stop |
| | | 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 { |
| | | # Both targets were proven absent before the transaction, so any |
| | | # surviving object belongs to this attempt even if a provider threw |
| | | # after partially creating it. |
| | | if (Test-Path -LiteralPath $registryPath) { |
| | | Remove-Item -LiteralPath $registryPath -Recurse -Force |
| | | $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.' |
| | | } |
| | | if (Test-Path -LiteralPath $root) { |
| | | Remove-Item -LiteralPath $root -Recurse -Force |
| | | $restoredRegistry = Get-RegistrySnapshot $registryPath $TestFileRegistryProvider.IsPresent |
| | | if (-not (Test-RegistrySnapshotExact $restoredRegistry $previousInstall.ManifestPath)) { |
| | | throw 'RECOVERY_REQUIRED: Native Messaging v026 registry preimage was not restored.' |
| | | } |
| | | throw |
| | | $null = Assert-ExactPreviousInstall @previousIdentityParameters |
| | | Remove-ProvenOwnedInstallRoot $root $installedHost $installedConfig $manifestPath $buildEntry $configIdentity |
| | | } catch { |
| | | throw "RECOVERY_REQUIRED: $($_.Exception.Message) Original failure: $($originalFailure.Message)" |
| | | } |
| | | throw $originalFailure |
| | | } |
| | | } |