#Requires -Version 2.0

# Batch apply hex edits
# The edits to apply come from a CSV file in the same path
# Reference: https://stackoverflow.com/questions/20935356/

# Handle HTTPS
[Net.ServicePointManager]::SecurityProtocol = "Tls12, Tls11, Tls, Ssl3"
# More cock-smoking boilerplate
[System.Reflection.Assembly]::LoadWithPartialName('System.Web.Extensions')

# There is [Environment]::Is64BitProcess since PowerShell 3.0
function Get-Architecture {
  param($FilePath)

  [int32]$MACHINE_OFFSET = 4
  [int32]$PE_POINTER_OFFSET = 60

  [byte[]]$data = New-Object -TypeName System.Byte[] -ArgumentList 4096
  $stream = New-Object -TypeName System.IO.FileStream -ArgumentList ($FilePath, 'Open', 'Read')
  $stream.Read($data, 0, 4096) | Out-Null

  [int32]$PE_HEADER_ADDR = [System.BitConverter]::ToInt32($data, $PE_POINTER_OFFSET)
  [int32]$machineUint = [System.BitConverter]::ToUInt16($data, $PE_HEADER_ADDR + $MACHINE_OFFSET)

  $result = ""

  switch ($machineUint) {
    0      { $result = "Native" }
    0x014c { $result = "x86" }
    0x0200 { $result = "Itanium" }
    0x8664 { $result = "x64" }
  }

  $result
}

# TODO Add warning about executing code from the Internet
function Get-Data {
  param (
    [Parameter(Mandatory = $True)]
    [String]
    $pathstring
  )
  $abspath = ConvertTo-AbsoluteUri($pathstring)
  $url = [System.Uri]::New($abspath)
  $domain = $url.Host
  If ($domain -match '^(github\.com|gist\.github|(cdn\.)?rawgit\.com)') {
    $user = $url.Segments[1].Replace('/', '')
    $id = $url.Segments[2].Replace('/', '')
    
    $client = New-Object System.Net.WebClient
    # Encourage your mom. It's fucking mandatory.
    # Otherwise you get protocol violation error
    $client.Headers.Add('Accept', 'application/vnd.github.v3+json')
    # User-Agent also fucking mandatory. Suck cock, GitHub, gimme the fucking data
    $client.Headers.Add('User-Agent', 'PowerShell')
    $json = $client.DownloadString('https://api.github.com/gists/' + $id)
    $ser = New-Object System.Web.Script.Serialization.JavaScriptSerializer
    $obj = $ser.DeserializeObject($json)
    # TODO WTF
    $dataurl = $obj.files.Keys | Where-Object { $obj.files.Item($_).language -eq 'CSV' } | Select -First 1 | % { $obj.files.Item($_).raw_url }
    Return $dataurl
  } Else {
    # TODO Prompt user
    Return $Null
  }
}

# Take care of short URL redirection
function ConvertTo-AbsoluteUri {
  param (
    [String]
    $pathstring
    )
  Return [System.Net.HttpWebRequest]::Create($pathstring).GetResponse().ResponseUri.AbsoluteUri
}

# Work in TEMP directory
Set-Location $env:TEMP
# Get URL from invocation and build CSV URL
$RemotePath = Get-Data($_)
# Store in user Temp folder with a random filename to avoid clashes
$LocalPath = $env:TEMP + '\' + [System.IO.Path]::GetRandomFileName()

# Request the file from the Gist
(New-Object System.Net.WebClient).DownloadFile($RemotePath, $LocalPath)
# Read the data from the CSV file
$entries = Import-Csv $LocalPath | Where-Object {$_.Platform -eq "Windows"}
# Get the program to work on
$target = $entries.Filename | Select-Object -First 1

# Try to find the target program on PATH
$program = Get-Command -ErrorAction SilentlyContinue $target | Get-Item
# If not ask user to input the path manually
while ($program -eq $null) {
  $userinput = Read-Host "Enter path for executable to patch"
  If (Test-Path $userinput -Include *.exe) {
    $program = Get-Item -Path $userinput
  } else {
    Write-Error "Invalid file path, re-enter."
    $program = $null
  }
}

$name = $program.VersionInfo.ProductName
$build = $program.VersionInfo.ProductVersion
$arch = Get-Architecture $program
# Get the edits to apply for your build version and architecture
$edits = $entries | Where-Object {$_.Build -eq $build -and $_.Architecture -eq $arch}

If ($edits) {
  # Kill if running
  Start-Process -FilePath tskill -ArgumentList $program.BaseName `
    -NoNewWindow -PassThru -Wait -RedirectStandardError nul | Out-Null
  # Backup
  try {
    $program.CopyTo($([IO.Path]::ChangeExtension($program, '.bak')), $false) | Out-Null
  } catch {
    Write-Output $error[0].Exception.InnerException
  } finally {
    Write-Output "Original file backed up"
  }

  $bytes = [System.IO.File]::ReadAllBytes($program)
  Write-Output "Patching $name build $build (Windows $arch)"

  For ($i=0; $i -lt $edits.length; $i++) {
    Write-Output "[$($i+1) of $($edits.length)] $($edits[$i].Offset) $($edits[$i].Original) > $($edits[$i].Modified)"
    # Patch
    $bytes[$edits[$i].Offset] = $edits[$i].Modified
  }

  # Write new bytes to file
  # Will fail if you have it selected in Explorer! Just try again.
  [System.IO.File]::WriteAllBytes($program, $bytes)

  # Extra: Output new program hash
  $hasher = [System.Security.Cryptography.HashAlgorithm]::Create('SHA256')
  $hash = $hasher.ComputeHash([System.IO.File]::ReadAllBytes($program))
  $hashString = [System.BitConverter]::ToString($hash).Replace('-', '')
  Write-Output "Patched!" "New hash: $hashString"
} Else {
  Write-Error "Could not locate patches."
}