408 lines
12 KiB
PowerShell
408 lines
12 KiB
PowerShell
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
|