param( [Parameter(Mandatory = $true)] [string]$ExecutablePath, [string]$ProjectToken = 'UnrealHyperTwist', [string[]]$AdditionalArguments = @( '-notraceserver', '-traceautostart=0' ), [string]$ReportPath = '', [switch]$VerifyOnly ) $ErrorActionPreference = 'Stop' $RawDataResourceType = 10 $BootstrapArgumentsResourceId = 202 function Write-Utf8JsonFile { param( [Parameter(Mandatory = $true)] [string]$Path, [Parameter(Mandatory = $true)] [object]$Value ) $ParentPath = Split-Path -Parent $Path if (-not [string]::IsNullOrWhiteSpace($ParentPath)) { New-Item -ItemType Directory -Force -Path $ParentPath | Out-Null } [System.IO.File]::WriteAllText( $Path, ($Value | ConvertTo-Json -Depth 10), (New-Object System.Text.UTF8Encoding($false)) ) } if (-not ('HyperTwistBootstrapResources.NativeMethods' -as [type])) { Add-Type -TypeDefinition @' using System; using System.Runtime.InteropServices; namespace HyperTwistBootstrapResources { public static class NativeMethods { [DllImport("kernel32.dll", CharSet = CharSet.Unicode, SetLastError = true)] public static extern IntPtr LoadLibraryEx( string fileName, IntPtr fileHandle, uint flags ); [DllImport("kernel32.dll", SetLastError = true)] public static extern bool FreeLibrary(IntPtr moduleHandle); [DllImport("kernel32.dll", SetLastError = true)] public static extern IntPtr FindResource( IntPtr moduleHandle, IntPtr resourceName, IntPtr resourceType ); [DllImport("kernel32.dll", SetLastError = true)] public static extern uint SizeofResource( IntPtr moduleHandle, IntPtr resourceInfo ); [DllImport("kernel32.dll", SetLastError = true)] public static extern IntPtr LoadResource( IntPtr moduleHandle, IntPtr resourceInfo ); [DllImport("kernel32.dll", SetLastError = true)] public static extern IntPtr LockResource(IntPtr resourceData); [DllImport("kernel32.dll", CharSet = CharSet.Unicode, SetLastError = true)] public static extern IntPtr BeginUpdateResource( string fileName, bool deleteExistingResources ); [DllImport("kernel32.dll", SetLastError = true)] public static extern bool UpdateResource( IntPtr updateHandle, IntPtr resourceType, IntPtr resourceName, ushort language, byte[] data, uint dataLength ); [DllImport("kernel32.dll", SetLastError = true)] public static extern bool EndUpdateResource( IntPtr updateHandle, bool discard ); } } '@ } function New-Win32Exception { param( [Parameter(Mandatory = $true)] [string]$Operation ) $ErrorCode = [Runtime.InteropServices.Marshal]::GetLastWin32Error() return New-Object ` -TypeName ComponentModel.Win32Exception ` -ArgumentList $ErrorCode, "$Operation failed with Win32 error $ErrorCode." } function Get-BootstrapArgumentsResource { param( [Parameter(Mandatory = $true)] [string]$Path ) $LoadLibraryAsDataFile = 0x00000002 $LoadLibraryAsImageResource = 0x00000020 $ModuleHandle = [HyperTwistBootstrapResources.NativeMethods]::LoadLibraryEx( $Path, [IntPtr]::Zero, ($LoadLibraryAsDataFile -bor $LoadLibraryAsImageResource) ) if ($ModuleHandle -eq [IntPtr]::Zero) { throw (New-Win32Exception -Operation "LoadLibraryEx('$Path')") } try { $ResourceInfo = [HyperTwistBootstrapResources.NativeMethods]::FindResource( $ModuleHandle, [IntPtr]$BootstrapArgumentsResourceId, [IntPtr]$RawDataResourceType ) if ($ResourceInfo -eq [IntPtr]::Zero) { throw ( New-Win32Exception -Operation ( "FindResource(type=$RawDataResourceType,id=$BootstrapArgumentsResourceId)" ) ) } $ResourceSize = [HyperTwistBootstrapResources.NativeMethods]::SizeofResource( $ModuleHandle, $ResourceInfo ) if ($ResourceSize -eq 0) { throw ( New-Win32Exception -Operation ( "SizeofResource(type=$RawDataResourceType,id=$BootstrapArgumentsResourceId)" ) ) } $LoadedResource = [HyperTwistBootstrapResources.NativeMethods]::LoadResource( $ModuleHandle, $ResourceInfo ) if ($LoadedResource -eq [IntPtr]::Zero) { throw ( New-Win32Exception -Operation ( "LoadResource(type=$RawDataResourceType,id=$BootstrapArgumentsResourceId)" ) ) } $ResourcePointer = [HyperTwistBootstrapResources.NativeMethods]::LockResource( $LoadedResource ) if ($ResourcePointer -eq [IntPtr]::Zero) { throw ( New-Win32Exception -Operation ( "LockResource(type=$RawDataResourceType,id=$BootstrapArgumentsResourceId)" ) ) } $ResourceBytes = New-Object byte[] $ResourceSize [Runtime.InteropServices.Marshal]::Copy( $ResourcePointer, $ResourceBytes, 0, [int]$ResourceSize ) return [Text.Encoding]::Unicode.GetString($ResourceBytes).TrimEnd([char]0) } finally { [void][HyperTwistBootstrapResources.NativeMethods]::FreeLibrary($ModuleHandle) } } function Set-BootstrapArgumentsResource { param( [Parameter(Mandatory = $true)] [string]$Path, [Parameter(Mandatory = $true)] [string]$Value ) $ResourceBytes = [Text.Encoding]::Unicode.GetBytes($Value + [char]0) $UpdateHandle = [HyperTwistBootstrapResources.NativeMethods]::BeginUpdateResource( $Path, $false ) if ($UpdateHandle -eq [IntPtr]::Zero) { throw (New-Win32Exception -Operation "BeginUpdateResource('$Path')") } $UpdateClosed = $false try { if (-not [HyperTwistBootstrapResources.NativeMethods]::UpdateResource( $UpdateHandle, [IntPtr]$RawDataResourceType, [IntPtr]$BootstrapArgumentsResourceId, [uint16]0, $ResourceBytes, [uint32]$ResourceBytes.Length )) { throw ( New-Win32Exception -Operation ( "UpdateResource(type=$RawDataResourceType,id=$BootstrapArgumentsResourceId)" ) ) } $UpdateCompleted = [HyperTwistBootstrapResources.NativeMethods]::EndUpdateResource( $UpdateHandle, $false ) $UpdateClosed = $true if (-not $UpdateCompleted) { throw (New-Win32Exception -Operation "EndUpdateResource('$Path')") } } finally { if (-not $UpdateClosed) { [void][HyperTwistBootstrapResources.NativeMethods]::EndUpdateResource( $UpdateHandle, $true ) } } } if (-not (Test-Path -LiteralPath $ExecutablePath -PathType Leaf)) { throw "HyperTwist bootstrap executable '$ExecutablePath' was not found." } if ([string]::IsNullOrWhiteSpace($ProjectToken)) { throw 'ProjectToken must not be empty.' } $NormalizedAdditionalArguments = @( $AdditionalArguments | ForEach-Object { if ($null -eq $_) { return } $_.Trim() } | Where-Object { -not [string]::IsNullOrWhiteSpace($_) } ) foreach ($Argument in $NormalizedAdditionalArguments) { if ($Argument.IndexOf([char]0) -ge 0 ` -or $Argument.IndexOf("`r") -ge 0 ` -or $Argument.IndexOf("`n") -ge 0) { throw "Bootstrap launch argument '$Argument' contains an unsupported control character." } } $ResolvedExecutablePath = (Resolve-Path -LiteralPath $ExecutablePath).Path $ExpectedArguments = @($ProjectToken) + $NormalizedAdditionalArguments $ExpectedResourceValue = $ExpectedArguments -join ' ' if ([string]::IsNullOrWhiteSpace($ReportPath)) { $ReportPath = "$ResolvedExecutablePath.bootstrap-launch-arguments.json" } $OriginalHash = (Get-FileHash -LiteralPath $ResolvedExecutablePath -Algorithm SHA256).Hash.ToLowerInvariant() $OriginalResourceValue = Get-BootstrapArgumentsResource -Path $ResolvedExecutablePath $Result = [ordered]@{ reportVersion = 'ht-bootstrap-launch-arguments/v1' generatedAtUtc = [DateTime]::UtcNow.ToString('o') executablePath = $ResolvedExecutablePath resourceType = $RawDataResourceType resourceId = $BootstrapArgumentsResourceId verifyOnly = [bool]$VerifyOnly expectedArguments = @($ExpectedArguments) expectedResourceValue = $ExpectedResourceValue originalResourceValue = $OriginalResourceValue finalResourceValue = $null originalSha256 = $OriginalHash finalSha256 = $null changed = $false result = 'failed' error = $null } $TemporaryPath = $null $BackupPath = $null try { if ($VerifyOnly) { if ($OriginalResourceValue -ne $ExpectedResourceValue) { throw ( "Bootstrap argument resource was '$OriginalResourceValue'; " + "expected '$ExpectedResourceValue'." ) } } elseif ($OriginalResourceValue -ne $ExpectedResourceValue) { $TemporaryPath = '{0}.bootstrap-{1}-{2}.tmp' -f ( $ResolvedExecutablePath, $PID, [Guid]::NewGuid().ToString('N') ) $BackupPath = '{0}.bootstrap-{1}-{2}.bak' -f ( $ResolvedExecutablePath, $PID, [Guid]::NewGuid().ToString('N') ) Copy-Item -LiteralPath $ResolvedExecutablePath -Destination $TemporaryPath -Force Set-BootstrapArgumentsResource -Path $TemporaryPath -Value $ExpectedResourceValue $TemporaryResourceValue = Get-BootstrapArgumentsResource -Path $TemporaryPath if ($TemporaryResourceValue -ne $ExpectedResourceValue) { throw ( "Patched bootstrap argument resource was '$TemporaryResourceValue'; " + "expected '$ExpectedResourceValue'." ) } [System.IO.File]::Replace( $TemporaryPath, $ResolvedExecutablePath, $BackupPath, $true ) $TemporaryPath = $null $Result.changed = $true } $Result.finalResourceValue = Get-BootstrapArgumentsResource -Path $ResolvedExecutablePath if ($Result.finalResourceValue -ne $ExpectedResourceValue) { throw ( "Final bootstrap argument resource was '$($Result.finalResourceValue)'; " + "expected '$ExpectedResourceValue'." ) } $Result.finalSha256 = ( Get-FileHash -LiteralPath $ResolvedExecutablePath -Algorithm SHA256 ).Hash.ToLowerInvariant() $Result.result = 'passed' } catch { $Result.error = $_.Exception.Message throw } finally { if (-not [string]::IsNullOrWhiteSpace($TemporaryPath) ` -and (Test-Path -LiteralPath $TemporaryPath)) { Remove-Item -LiteralPath $TemporaryPath -Force -ErrorAction SilentlyContinue } if (-not [string]::IsNullOrWhiteSpace($BackupPath) ` -and (Test-Path -LiteralPath $BackupPath)) { Remove-Item -LiteralPath $BackupPath -Force -ErrorAction SilentlyContinue } Write-Utf8JsonFile -Path $ReportPath -Value $Result } $Result | ConvertTo-Json -Depth 10