Files
RustRaptor/release/Install-Linux.sh
T

116 lines
5.8 KiB
Bash
Executable File

#!/usr/bin/env bash
set -euo pipefail
[[ $(uname -s) == Linux && $(uname -m) == x86_64 ]] || { echo 'This package requires Linux x86_64.' >&2; exit 1; }
command -v python3 >/dev/null || { echo 'Install Python 3 to use this download helper. The game itself does not require Python.' >&2; exit 1; }
python3 - "${1:-$PWD}" <<'PYTHON'
# BEGIN INSTALLER PYTHON
import hashlib
import json
import os
from pathlib import Path
import re
import shutil
import stat
import sys
import tempfile
import urllib.request
import zipfile
TAG = 'v0.1.0-preview.20261020'
BASE = 'https://github.com/Quintine/RustRaptor/releases/download/' + TAG
def safe_name(name):
if not isinstance(name, str) or not name.startswith('RustRaptor/') or '\\' in name:
raise ValueError('Unsafe ZIP member path')
parts = name.split('/')
if any(not p or p in ('.', '..') or ':' in p or any(ord(c) < 32 for c in p) for p in parts):
raise ValueError('Unsafe ZIP member path')
return name
def extract(archive_path, destination, files):
expected = {safe_name('RustRaptor/' + row['name']): row for row in files}
if len(expected) != len(files):
raise ValueError('Duplicate planned ZIP members')
with zipfile.ZipFile(archive_path) as archive:
if len(archive.infolist()) != len(expected) or set(archive.namelist()) != set(expected):
raise ValueError('ZIP membership differs from frozen release manifest')
for member in archive.infolist():
safe_name(member.filename)
mode = member.external_attr >> 16
if member.is_dir() or stat.S_IFMT(mode) not in (0, stat.S_IFREG):
raise ValueError('ZIP member is not an ordinary file')
row = expected[member.filename]
if member.file_size != row['bytes'] or not re.fullmatch('[0-9a-f]{64}', row['sha256']):
raise ValueError('Invalid member size or SHA-256')
target = destination / member.filename
# The installation root must be fresh; refuse links and collisions.
parent = destination
for part in Path(member.filename).parts[:-1]:
parent /= part
if parent.is_symlink() or (parent.exists() and not parent.is_dir()):
raise ValueError('Unsafe installation parent')
parent.mkdir(exist_ok=True)
digest = hashlib.sha256()
with target.open('xb') as output, archive.open(member) as source:
while block := source.read(128 * 1024):
digest.update(block)
output.write(block)
if digest.hexdigest() != row['sha256']:
raise ValueError('Installed payload SHA-256 differs from frozen manifest')
target.chmod(0o755 if mode & 0o111 else 0o644)
def install(destination):
destination.mkdir(parents=True, exist_ok=True)
destination = destination.resolve(strict=True)
if (destination / 'RustRaptor').exists() or (destination / 'RustRaptor').is_symlink():
raise ValueError('Use a directory without an existing RustRaptor folder')
with tempfile.TemporaryDirectory(prefix='RustRaptor-download-') as temp:
cache = Path(temp)
urllib.request.urlretrieve(BASE + '/release-manifest.json', cache / 'manifest.json')
manifest = json.loads((cache / 'manifest.json').read_bytes())
if manifest['format_version'] != 1 or manifest['tag'] != TAG:
raise ValueError('Wrong release manifest version or tag')
assets = [a for a in manifest['assets'] if a['kind'] in ('shared-data', 'linux')]
names = [a['name'] for a in assets]
data = [a for a in assets if a['kind'] == 'shared-data']
if names.count('RustRaptor-linux-x86_64.zip') != 1 or not data or len(set(names)) != len(names):
raise ValueError('Incomplete release asset manifest')
required = {f'RustRaptor-data-{i:02d}-of-{len(data):02d}.zip' for i in range(1, len(data) + 1)}
if {a['name'] for a in data} != required:
raise ValueError('Incomplete data-volume sequence')
all_files = [row['name'].casefold() for a in assets for row in a['files']]
if len(set(all_files)) != len(all_files):
raise ValueError('Duplicate installation paths across archives')
needed = sum(row['bytes'] for a in assets for row in a['files'])
if shutil.disk_usage(destination).free < needed + 128 * 1024**2:
raise ValueError('Insufficient space for the complete installation')
for asset in assets:
name = asset['name']
if name != 'RustRaptor-linux-x86_64.zip' and name not in required:
raise ValueError('Unsafe release asset name')
if not re.fullmatch('[0-9a-f]{64}', asset['sha256']) or not 0 < asset['bytes'] < 2 * 1024**3:
raise ValueError('Invalid release asset SHA-256 or size')
if shutil.disk_usage(cache).free < asset['bytes'] + 128 * 1024**2:
raise ValueError('Insufficient temporary space for one download')
print('Downloading ' + name, flush=True)
archive_path = cache / name
urllib.request.urlretrieve(BASE + '/' + name, archive_path)
with archive_path.open('rb') as source:
digest = hashlib.sha256()
while block := source.read(128 * 1024):
digest.update(block)
if archive_path.stat().st_size != asset['bytes'] or digest.hexdigest() != asset['sha256']:
raise ValueError('Downloaded archive size or SHA-256 mismatch: ' + name)
extract(archive_path, destination, asset['files'])
archive_path.unlink()
print('Installed. Start: ' + str(destination / 'RustRaptor/run.sh'))
if __name__ == '__main__':
install(Path(sys.argv[1]))
# END INSTALLER PYTHON
PYTHON