149 lines
8.4 KiB
PowerShell
149 lines
8.4 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
|
|
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
|
|
}
|