[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)]
|
[ValidateSet('none', 'after-root', 'after-payload', 'after-config', 'after-manifest', 'after-registry-key', 'after-registry-value')]
|
[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.0.0+20260805.v002'
|
$expectedHostBuild = 'project-info-bili-auth-native-host/1.0.0+20260805.v002'
|
$expectedHostExecutable = 'project-info-bili-auth-native-host.exe'
|
$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 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()
|
}
|
}
|
|
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',
|
'install_native_host.ps1', 'job.py', 'manifest.json',
|
'native-host-manifest.template.json', 'native_host.py', 'protocol.py',
|
'sidepanel.css', 'sidepanel.html', 'sidepanel.js', 'worker.py'
|
)
|
Assert-ExactSourceTree $sourceRoot $sourceManifestPath $expectedSourceFiles
|
$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
|
$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.target -cne 'BV1HA3o6oEJJ' -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.'
|
}
|
$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'
|
)
|
$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') {
|
throw 'Host config identity mismatch.'
|
}
|
foreach ($name in @('ffmpeg', 'ffprobe', 'bridge_python', 'bridge_script', 'batch_json', '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 (Get-ChildItem -LiteralPath $resolvedDestination -File | Where-Object { $_.Name.StartsWith('BV1HA3o6oEJJ.', [StringComparison]::OrdinalIgnoreCase) }) {
|
throw 'Formal output already exists; overwrite is forbidden.'
|
}
|
if (Test-Path -LiteralPath $root) {
|
throw 'InstallRoot already exists; overwrite is forbidden.'
|
}
|
|
$registryPath = "HKCU:\Software\Google\Chrome\NativeMessagingHosts\$expectedHostName"
|
if ($TestFileRegistryProvider) {
|
if (-not $TestRegistryRoot) {
|
throw 'TestRegistryRoot is required for the test file registry provider.'
|
}
|
$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.'
|
}
|
$registryPath = Join-Path $testRegistryBase $expectedHostName
|
} elseif ($TestRegistryRoot -or $InjectFailure -cne 'none') {
|
throw 'Test-only controls require TestFileRegistryProvider.'
|
}
|
if (Test-Path -LiteralPath $registryPath) {
|
throw 'Native Messaging registration already exists; overwrite is forbidden.'
|
}
|
|
if (-not $Install) {
|
[pscustomobject]@{
|
result = 'VALIDATION_PASS_ONLY'
|
extension_id = $expectedId
|
origin = $expectedOrigin
|
host_name = $expectedHostName
|
packaging = 'pyinstaller-onefile'
|
} | ConvertTo-Json -Compress
|
return
|
}
|
|
if ($PSCmdlet.ShouldProcess($root, 'Install exact-BVID Native Messaging host for current user')) {
|
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 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'
|
$manifestPath = Join-Path $root 'native-host-manifest.json'
|
$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)
|
$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.'
|
}
|
Stop-Injected 'after-manifest'
|
|
if ($TestFileRegistryProvider) {
|
[System.IO.Directory]::CreateDirectory($registryPath) | Out-Null
|
} else {
|
New-Item -Path $registryPath -ErrorAction Stop | Out-Null
|
}
|
Stop-Injected 'after-registry-key'
|
if ($TestFileRegistryProvider) {
|
Write-Utf8CreateNew (Join-Path $registryPath 'default.value') $manifestPath
|
} else {
|
Set-Item -LiteralPath $registryPath -Value $manifestPath -ErrorAction Stop
|
}
|
Stop-Injected 'after-registry-value'
|
} 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
|
}
|
if (Test-Path -LiteralPath $root) {
|
Remove-Item -LiteralPath $root -Recurse -Force
|
}
|
throw
|
}
|
}
|