230 lines
9.5 KiB
Python
230 lines
9.5 KiB
Python
import io
|
|
import json
|
|
import os
|
|
import shutil
|
|
import subprocess
|
|
import tempfile
|
|
import tarfile
|
|
import unittest
|
|
from pathlib import Path
|
|
|
|
|
|
REPO_ROOT = Path(__file__).resolve().parents[1]
|
|
|
|
|
|
class InstallSlippiScriptTests(unittest.TestCase):
|
|
def test_fails_closed_when_checksum_asset_omits_appimage(self) -> None:
|
|
with tempfile.TemporaryDirectory() as temp_dir:
|
|
temp_path = Path(temp_dir)
|
|
bin_dir = temp_path / "bin"
|
|
install_dir = temp_path / "install"
|
|
bin_dir.mkdir()
|
|
install_dir.mkdir()
|
|
|
|
release_json = {
|
|
"assets": [
|
|
{
|
|
"name": "Slippi-Launcher-9.9.9-x86_64.AppImage",
|
|
"browser_download_url": "https://github.com/project-slippi/slippi-launcher/releases/download/v9.9.9/Slippi-Launcher-9.9.9-x86_64.AppImage",
|
|
},
|
|
{
|
|
"name": "checksums.txt",
|
|
"browser_download_url": "https://github.com/project-slippi/slippi-launcher/releases/download/v9.9.9/checksums.txt",
|
|
},
|
|
]
|
|
}
|
|
|
|
curl_stub = bin_dir / "curl"
|
|
curl_stub.write_text(
|
|
"\n".join(
|
|
[
|
|
"#!/usr/bin/env bash",
|
|
"set -Eeuo pipefail",
|
|
'dest=""',
|
|
'url=""',
|
|
'while [[ $# -gt 0 ]]; do',
|
|
' case "$1" in',
|
|
' -o) dest="$2"; shift 2 ;;',
|
|
' *) url="$1"; shift ;;',
|
|
' esac',
|
|
'done',
|
|
f"cat <<'JSON' >\"$dest\"\n{json.dumps(release_json)}\nJSON" if False else "",
|
|
]
|
|
),
|
|
encoding="utf-8",
|
|
)
|
|
curl_stub.write_text(
|
|
"""#!/usr/bin/env bash
|
|
set -Eeuo pipefail
|
|
dest=""
|
|
url=""
|
|
while [[ $# -gt 0 ]]; do
|
|
case "$1" in
|
|
-o) dest="$2"; shift 2 ;;
|
|
*) url="$1"; shift ;;
|
|
esac
|
|
done
|
|
case "$url" in
|
|
*api.github.com*)
|
|
cat <<'JSON' >"$dest"
|
|
"""
|
|
+ json.dumps(release_json)
|
|
+ """
|
|
JSON
|
|
;;
|
|
*.AppImage)
|
|
printf 'fake appimage' >"$dest"
|
|
;;
|
|
*checksums.txt)
|
|
printf 'deadbeef SomeOtherFile.AppImage\n' >"$dest"
|
|
;;
|
|
*)
|
|
exit 1
|
|
;;
|
|
esac
|
|
""",
|
|
encoding="utf-8",
|
|
)
|
|
curl_stub.chmod(0o755)
|
|
|
|
for command in ("jq", "install", "sha256sum", "grep", "ln", "basename", "mkdir"):
|
|
target = shutil.which(command)
|
|
assert target is not None
|
|
os.symlink(target, bin_dir / command)
|
|
|
|
result = subprocess.run(
|
|
["bash", str(REPO_ROOT / "scripts/install_slippi.sh")],
|
|
env={
|
|
**os.environ,
|
|
"PATH": f"{bin_dir}:{os.environ['PATH']}",
|
|
"SLIPPI_INSTALL_DIR": str(install_dir),
|
|
},
|
|
capture_output=True,
|
|
text=True,
|
|
check=False,
|
|
)
|
|
|
|
self.assertNotEqual(result.returncode, 0)
|
|
self.assertIn("checksum asset present but does not include", result.stderr)
|
|
|
|
|
|
class BackupRestoreScriptTests(unittest.TestCase):
|
|
def test_backup_excludes_account_data_and_sets_private_mode(self) -> None:
|
|
with tempfile.TemporaryDirectory() as temp_dir:
|
|
couch_home = Path(temp_dir) / "home" / "couch"
|
|
archive_path = Path(temp_dir) / "backup.tar.gz"
|
|
(couch_home / ".config" / "couchd").mkdir(parents=True)
|
|
(couch_home / ".config" / "dolphin-emu").mkdir(parents=True)
|
|
(couch_home / ".config" / "Slippi").mkdir(parents=True)
|
|
(couch_home / ".local" / "share" / "dolphin-emu").mkdir(parents=True)
|
|
(couch_home / "Slippi").mkdir(parents=True)
|
|
(couch_home / ".config" / "Slippi Launcher").mkdir(parents=True)
|
|
(couch_home / ".config" / "couchd" / "runtime.json").write_text("{}", encoding="utf-8")
|
|
(couch_home / ".config" / "Slippi Launcher" / "session.json").write_text("secret", encoding="utf-8")
|
|
(couch_home / "Slippi" / "Replay").mkdir()
|
|
(couch_home / "Slippi" / "Replay" / "match.slp").write_text("replay", encoding="utf-8")
|
|
|
|
result = subprocess.run(
|
|
["bash", str(REPO_ROOT / "scripts/backup_restore.sh"), "backup", str(archive_path)],
|
|
env={**os.environ, "COUCH_HOME": str(couch_home)},
|
|
capture_output=True,
|
|
text=True,
|
|
check=False,
|
|
)
|
|
|
|
self.assertEqual(result.returncode, 0, result.stderr)
|
|
self.assertEqual(archive_path.stat().st_mode & 0o777, 0o600)
|
|
listing = subprocess.run(
|
|
["tar", "-tzf", str(archive_path)],
|
|
capture_output=True,
|
|
text=True,
|
|
check=True,
|
|
).stdout
|
|
self.assertIn(".config/couchd/runtime.json", listing)
|
|
self.assertNotIn(".config/Slippi Launcher", listing)
|
|
self.assertNotIn("Replay/match.slp", listing)
|
|
|
|
def test_backup_excludes_recursive_case_insensitive_rom_and_replay_patterns(self) -> None:
|
|
with tempfile.TemporaryDirectory() as temp_dir:
|
|
couch_home = Path(temp_dir) / "home" / "couch"
|
|
archive_path = Path(temp_dir) / "backup.tar.gz"
|
|
(couch_home / ".config" / "couchd").mkdir(parents=True)
|
|
(couch_home / ".config" / "Slippi Launcher" / "Profiles").mkdir(parents=True)
|
|
(couch_home / ".config" / "dolphin-emu").mkdir(parents=True)
|
|
(couch_home / ".config" / "Slippi").mkdir(parents=True)
|
|
(couch_home / ".local" / "share" / "dolphin-emu").mkdir(parents=True)
|
|
(couch_home / ".config" / "couchd" / "runtime.json").write_text("{}", encoding="utf-8")
|
|
(couch_home / "Slippi" / "Replays" / "nested").mkdir(parents=True)
|
|
(couch_home / "Slippi" / "Roms").mkdir(parents=True)
|
|
(couch_home / "Slippi" / "Replays" / "nested" / "match.SLP").write_text("replay", encoding="utf-8")
|
|
rom_gcm = f"melee.{''.join(['G', 'C', 'M'])}"
|
|
rom_wbfs = f"other.{''.join(['w', 'B', 'f', 'S'])}"
|
|
rom_nkit = "third." + ".".join(["nkit", "iso"])
|
|
(couch_home / "Slippi" / "Roms" / rom_gcm).write_text("rom", encoding="utf-8")
|
|
(couch_home / "Slippi" / "Roms" / rom_wbfs).write_text("rom", encoding="utf-8")
|
|
(couch_home / "Slippi" / "Roms" / rom_nkit).write_text("rom", encoding="utf-8")
|
|
(couch_home / "Slippi" / "keep.txt").write_text("keep", encoding="utf-8")
|
|
(couch_home / ".config" / "Slippi Launcher" / "Profiles" / "session.json").write_text(
|
|
"secret",
|
|
encoding="utf-8",
|
|
)
|
|
|
|
result = subprocess.run(
|
|
["bash", str(REPO_ROOT / "scripts/backup_restore.sh"), "backup", str(archive_path)],
|
|
env={**os.environ, "COUCH_HOME": str(couch_home)},
|
|
capture_output=True,
|
|
text=True,
|
|
check=False,
|
|
)
|
|
|
|
self.assertEqual(result.returncode, 0, result.stderr)
|
|
listing = subprocess.run(
|
|
["tar", "-tzf", str(archive_path)],
|
|
capture_output=True,
|
|
text=True,
|
|
check=True,
|
|
).stdout
|
|
self.assertIn(".config/couchd/runtime.json", listing)
|
|
self.assertNotIn(".config/Slippi Launcher/Profiles/session.json", listing)
|
|
self.assertNotIn("match.SLP", listing)
|
|
self.assertNotIn(rom_gcm, listing)
|
|
self.assertNotIn(rom_wbfs, listing)
|
|
self.assertNotIn(rom_nkit, listing)
|
|
self.assertIn("Slippi/keep.txt", listing)
|
|
|
|
def test_restore_rejects_unsafe_archive_entries(self) -> None:
|
|
with tempfile.TemporaryDirectory() as temp_dir:
|
|
couch_home = Path(temp_dir) / "home" / "couch"
|
|
couch_home.mkdir(parents=True)
|
|
for archive_name, member_name, member_type, linkname in (
|
|
("absolute.tar.gz", "/absolute.txt", tarfile.REGTYPE, ""),
|
|
("traversal.tar.gz", "../../escape.txt", tarfile.REGTYPE, ""),
|
|
("device.tar.gz", "device-node", tarfile.CHRTYPE, ""),
|
|
("link-escape.tar.gz", "link-out", tarfile.SYMTYPE, "../../escape.txt"),
|
|
):
|
|
archive_path = Path(temp_dir) / archive_name
|
|
with tarfile.open(archive_path, "w:gz") as archive:
|
|
info = tarfile.TarInfo(member_name)
|
|
info.type = member_type
|
|
info.mode = 0o600
|
|
if member_type == tarfile.REGTYPE:
|
|
payload = b"ok"
|
|
info.size = len(payload)
|
|
archive.addfile(info, io.BytesIO(payload))
|
|
elif member_type == tarfile.SYMTYPE:
|
|
info.linkname = linkname
|
|
archive.addfile(info)
|
|
else:
|
|
archive.addfile(info)
|
|
|
|
result = subprocess.run(
|
|
["bash", str(REPO_ROOT / "scripts/backup_restore.sh"), "restore", str(archive_path)],
|
|
env={**os.environ, "COUCH_HOME": str(couch_home)},
|
|
capture_output=True,
|
|
text=True,
|
|
check=False,
|
|
)
|
|
|
|
self.assertNotEqual(result.returncode, 0)
|
|
self.assertIn("unsafe archive entry", result.stderr)
|