Add verified multipart Linux and Windows GitHub releases
This commit is contained in:
1 parent
443d77852b
commit
d1f5b7e5a2
8 files changed
+1152
-1
No files matched your search
@@ -0,0 +1,147 @@
|
||||
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
|
||||
}
|
||||
Reference in new issue
Block a user