Files
RustRaptor/release/Install-Windows.ps1
T

148 lines
8.3 KiB
PowerShell

param([string]$Destination = (Get-Location).Path)
$ErrorActionPreference = 'Stop'
$tag = 'v0.1.0-preview.20261020'
$base = "https://github.com/Quintine/RustRaptor/releases/download/$tag"
if (-not [Environment]::Is64BitOperatingSystem) { throw 'RustRaptor requires 64-bit Windows.' }
[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12
# Use wide Win32 APIs and extended paths: Windows PowerShell 5.1 Expand-Archive
# cannot reliably extract the complete accepted proof/lineage long filenames.
if (-not ('RaptorLongPath' -as [type])) {
Add-Type -TypeDefinition @'
using System;
using System.IO;
using System.Runtime.InteropServices;
using System.ComponentModel;
using Microsoft.Win32.SafeHandles;
public static class RaptorLongPath {
[DllImport("kernel32.dll", CharSet=CharSet.Unicode, ExactSpelling=true, SetLastError=true)]
static extern uint GetFileAttributesW(string name);
[DllImport("kernel32.dll", CharSet=CharSet.Unicode, ExactSpelling=true, SetLastError=true)]
static extern bool CreateDirectoryW(string name, IntPtr security);
[DllImport("kernel32.dll", CharSet=CharSet.Unicode, ExactSpelling=true, SetLastError=true)]
static extern SafeFileHandle CreateFileW(string name, uint access, uint share, IntPtr security, uint creation, uint flags, IntPtr template);
static string Extended(string path) {
if (path.StartsWith(@"\\?\")) return path;
if (path.StartsWith(@"\\")) return @"\\?\UNC\" + path.Substring(2);
return @"\\?\" + path;
}
public static bool Exists(string path) { return GetFileAttributesW(Extended(path)) != 0xffffffff; }
public static void Directory(string path) {
string name = Extended(path);
uint attributes = GetFileAttributesW(name);
if (attributes == 0xffffffff) {
if (!CreateDirectoryW(name, IntPtr.Zero)) throw new Win32Exception(Marshal.GetLastWin32Error());
attributes = GetFileAttributesW(name);
}
if ((attributes & 0x400) != 0 || (attributes & 0x10) == 0) throw new IOException("Unsafe installation directory");
}
public static void Extract(Stream input, string path, long expectedSize, string expectedHash) {
using (SafeFileHandle handle = CreateFileW(Extended(path), 0x40000000, 0, IntPtr.Zero, 1, 0x80, IntPtr.Zero)) {
if (handle.IsInvalid) throw new Win32Exception(Marshal.GetLastWin32Error());
using (FileStream output = new FileStream(handle, FileAccess.Write))
using (var sha = System.Security.Cryptography.SHA256.Create()) {
byte[] buffer = new byte[131072]; long count = 0; int read;
while ((read = input.Read(buffer, 0, buffer.Length)) != 0) {
count += read; if (count > expectedSize) throw new IOException("Payload exceeds planned size");
sha.TransformBlock(buffer, 0, read, buffer, 0); output.Write(buffer, 0, read);
}
sha.TransformFinalBlock(new byte[0], 0, 0);
string hash = BitConverter.ToString(sha.Hash).Replace("-", "").ToLowerInvariant();
if (count != expectedSize || hash != expectedHash) throw new IOException("Installed payload size or SHA-256 mismatch");
}
}
}
}
'@
}
function Assert-Member([string]$Name) {
if (-not $Name.StartsWith('RustRaptor/') -or $Name.Contains('\')) { throw 'Unsafe ZIP member path.' }
foreach ($part in $Name.Split('/')) {
if ($part -eq '' -or $part -eq '.' -or $part -eq '..' -or $part -match '[<>:"|?*\x00-\x1f]' -or
$part.EndsWith('.') -or $part.EndsWith(' ') -or $part -match '^(CON|PRN|AUX|NUL|COM[1-9]|LPT[1-9])(\.|$)') {
throw 'Unsafe or nonportable ZIP member path.'
}
}
}
New-Item -ItemType Directory -Force -Path $Destination | Out-Null
$Destination = (Resolve-Path -LiteralPath $Destination).Path
[RaptorLongPath]::Directory($Destination)
$installation = $Destination.TrimEnd('\') + '\RustRaptor'
if ([RaptorLongPath]::Exists($installation)) { throw 'Use a directory without an existing RustRaptor folder.' }
$cache = Join-Path ([IO.Path]::GetTempPath()) ('RustRaptor-download-' + [guid]::NewGuid().ToString('N'))
New-Item -ItemType Directory -Path $cache | Out-Null
try {
$manifestPath = Join-Path $cache 'release-manifest.json'
Invoke-WebRequest -UseBasicParsing -Uri "$base/release-manifest.json" -OutFile $manifestPath
$manifest = Get-Content -LiteralPath $manifestPath -Raw | ConvertFrom-Json
if ($manifest.format_version -ne 1 -or $manifest.tag -cne $tag) { throw 'Wrong release manifest version or tag.' }
$assets = @($manifest.assets | Where-Object { $_.kind -in @('shared-data', 'windows') })
$data = @($assets | Where-Object { $_.kind -eq 'shared-data' })
$names = @{}
$paths = @{}
$platformCount = 0
$required = @{}
for ($i = 1; $i -le $data.Count; $i++) { $required.Add(('RustRaptor-data-{0:D2}-of-{1:D2}.zip' -f $i, $data.Count), $true) }
foreach ($asset in $assets) {
if ($names.ContainsKey($asset.name)) { throw 'Duplicate release asset name.' }
$names.Add($asset.name, $true)
if ($asset.kind -eq 'windows') {
if ($asset.name -cne 'RustRaptor-windows-x86_64.zip') { throw 'Wrong platform archive name.' }
$platformCount++
} elseif (-not $required.ContainsKey($asset.name)) { throw 'Incomplete data-volume sequence.' }
if ($asset.sha256 -cnotmatch '^[0-9a-f]{64}$' -or $asset.bytes -le 0 -or $asset.bytes -ge 2147483648) { throw 'Invalid asset size or SHA-256.' }
foreach ($row in $asset.files) {
Assert-Member ('RustRaptor/' + $row.name)
if ($paths.ContainsKey($row.name)) { throw 'Duplicate installation path across archives.' }
$paths.Add($row.name, $true)
if ($row.bytes -lt 0 -or $row.sha256 -cnotmatch '^[0-9a-f]{64}$') { throw 'Invalid payload size or SHA-256.' }
}
}
if ($platformCount -ne 1 -or $data.Count -eq 0 -or $assets.Count -ne ($data.Count + 1)) { throw 'Incomplete release asset manifest.' }
Add-Type -AssemblyName System.IO.Compression.FileSystem
foreach ($asset in $assets) {
$name = $asset.name
Write-Host "Downloading $name"
$archivePath = Join-Path $cache $name
Invoke-WebRequest -UseBasicParsing -Uri "$base/$name" -OutFile $archivePath
if ((Get-Item -LiteralPath $archivePath).Length -ne $asset.bytes -or
(Get-FileHash -LiteralPath $archivePath -Algorithm SHA256).Hash.ToLowerInvariant() -cne $asset.sha256) {
throw "Downloaded archive size or SHA-256 mismatch: $name"
}
$expected = @{}
foreach ($row in $asset.files) { $expected.Add(('RustRaptor/' + $row.name), $row) }
$archive = [IO.Compression.ZipFile]::OpenRead($archivePath)
try {
if ($archive.Entries.Count -ne $expected.Count) { throw 'ZIP membership differs from frozen manifest.' }
$seen = @{}
foreach ($entry in $archive.Entries) {
Assert-Member $entry.FullName
if (-not $expected.ContainsKey($entry.FullName) -or $seen.ContainsKey($entry.FullName)) { throw 'Unexpected or duplicate ZIP member.' }
$seen.Add($entry.FullName, $true)
$row = $expected[$entry.FullName]
$mode = ($entry.ExternalAttributes -shr 16) -band 0xF000
if ($mode -ne 0 -and $mode -ne 0x8000) { throw 'ZIP member is not an ordinary file.' }
if ($entry.Length -ne $row.bytes) { throw 'ZIP member size differs from frozen manifest.' }
}
foreach ($entry in $archive.Entries) {
$parts = $entry.FullName.Split('/')
$parent = $Destination.TrimEnd('\')
for ($i = 0; $i -lt ($parts.Length - 1); $i++) {
$parent += '\' + $parts[$i]
[RaptorLongPath]::Directory($parent)
}
$target = $parent + '\' + $parts[$parts.Length - 1]
$source = $entry.Open()
try { [RaptorLongPath]::Extract($source, $target, $expected[$entry.FullName].bytes, $expected[$entry.FullName].sha256) }
finally { $source.Dispose() }
}
} finally { $archive.Dispose() }
Remove-Item -LiteralPath $archivePath
}
Write-Host ('Installed. Start ' + $installation + '\run.cmd')
} catch {
Write-Warning 'Installation did not complete. Any partial RustRaptor folder has been preserved; move or remove it before retrying.'
throw
} finally {
Remove-Item -LiteralPath $cache -Recurse -Force
}