Files
AnyPS5/tools/tests/test_build_web.py
T

238 lines
12 KiB
Python

import os
import contextlib
import hashlib
import io
import json
from pathlib import Path
import subprocess
import struct
import sys
import tempfile
import unittest
from unittest.mock import patch
import warnings
import zipfile
import zlib
sys.path.insert(0, str(Path(__file__).resolve().parents[1]))
import build_web
def library_zip(entries):
buffer = io.BytesIO()
with warnings.catch_warnings(), zipfile.ZipFile(buffer, "w", compression=zipfile.ZIP_DEFLATED) as archive:
warnings.simplefilter("ignore", UserWarning)
for name, data in entries:
archive.writestr(name, data)
return buffer.getvalue()
class LibraryPublicationTests(unittest.TestCase):
def test_unicode_path_rewrite_is_rejected_before_decompression(self):
original = "libs/../libc.prx"
rewritten = "libs/libc.prx"
entry = zipfile.ZipInfo(original)
unicode_path = b"\x01" + struct.pack("<I", zlib.crc32(original.encode())) + rewritten.encode()
entry.extra = struct.pack("<HH", 0x7075, len(unicode_path)) + unicode_path
invalid = library_zip([(entry, b"content")])
with zipfile.ZipFile(io.BytesIO(invalid)) as archive:
decoded = archive.infolist()[0]
self.assertEqual(decoded.orig_filename, original)
if decoded.filename == original:
self.skipTest("zipfile does not interpret Unicode Path extra fields")
self.assertEqual(decoded.filename, rewritten)
valid = library_zip([("libs/libc.prx", b"valid")])
with patch.object(zipfile.ZipFile, "testzip", side_effect=AssertionError("archive decompressed")):
with self.assertRaisesRegex(ValueError, "Invalid library archive layout"):
self.publish(invalid, valid)
self.assertEqual(self.manifest.read_bytes(), self.previous)
self.assertEqual(list(self.libraries.iterdir()), [self.existing])
def test_original_archive_paths_are_rejected_before_decompression(self):
valid = library_zip([("libs/libc.prx", b"valid")])
for name in ("libs/libc.prx\0ignored", "libs/sub\0dir/libc.prx",
"libs\\libc.prx", "libs/sub\\libc.prx"):
with self.subTest(name=name):
placeholder = name.replace("\0", "!").replace("\\", "!")
invalid = library_zip([(placeholder, b"content")])
invalid = invalid.replace(placeholder.encode(), name.encode())
with zipfile.ZipFile(io.BytesIO(invalid)) as archive:
self.assertEqual(archive.infolist()[0].orig_filename, name)
with patch.object(zipfile.ZipFile, "testzip", side_effect=AssertionError("archive decompressed")):
with self.assertRaisesRegex(ValueError, "Invalid library archive layout"):
self.publish(invalid, valid)
self.assertEqual(self.manifest.read_bytes(), self.previous)
self.assertEqual(list(self.libraries.iterdir()), [self.existing])
def test_rewritten_path_in_second_platform_preserves_previous_manifest(self):
valid = library_zip([("libs/libc.prx", b"valid")])
invalid = library_zip([("libs/libc.prx!ignored", b"content")])
invalid = invalid.replace(b"libs/libc.prx!ignored", b"libs/libc.prx\0ignored")
with self.assertRaisesRegex(ValueError, "Invalid library archive layout"):
self.publish(valid, invalid)
self.assertEqual(self.manifest.read_bytes(), self.previous)
self.assertEqual(self.existing.read_bytes(), b"previous archive")
self.assertFalse(any(file.suffix == ".download" for file in self.libraries.iterdir()))
def test_unicode_archive_paths_are_published_unchanged(self):
payload = library_zip([("libs/\u65e5\u672c\u8a9e/\u00e9.prx", b"content"),
("libs/licenses/Third party.txt", b"license")])
self.publish(payload, payload)
manifest = json.loads(self.manifest.read_text(encoding="utf-8"))
for asset in manifest["platforms"].values():
self.assertEqual((self.output / asset["path"]).read_bytes(), payload)
def test_expanded_limit_is_checked_before_decompression(self):
payload = library_zip([("libs/libc.prx", b"a" * 4097)])
with patch.object(build_web, "MAX_EXPANDED_BYTES", 4096, create=True), \
patch.object(zipfile.ZipFile, "testzip", side_effect=AssertionError("archive decompressed")):
with self.assertRaisesRegex(ValueError, "expanded size"):
self.publish(payload, payload)
self.assertEqual(self.manifest.read_bytes(), self.previous)
self.assertEqual(list(self.libraries.iterdir()), [self.existing])
def test_expanded_limit_boundary_can_be_published(self):
payload = library_zip([("libs/libc.prx", b"a" * 4096)])
with patch.object(build_web, "MAX_EXPANDED_BYTES", 4096, create=True):
self.publish(payload, payload)
report = json.loads(self.manifest.read_text(encoding="utf-8"))
self.assertTrue(all(asset["expandedBytes"] == 4096 for asset in report["platforms"].values()))
def test_file_ancestors_are_rejected_before_publication(self):
valid = library_zip([("libs/libc.prx", b"valid")])
for entries in (("libs/nested", "libs/nested/libc.prx"),
("libs/NESTED/libc.prx", "libs/nested")):
with self.subTest(entries=entries):
invalid = library_zip([(name, b"content") for name in entries])
with self.assertRaisesRegex(ValueError, "file is also used as a directory"):
self.publish(invalid, valid)
self.assertEqual(self.manifest.read_bytes(), self.previous)
self.assertEqual(list(self.libraries.iterdir()), [self.existing])
def test_explicit_and_implicit_archive_directories_are_valid(self):
for entries in ([('libs/sub/libc.prx', b'content')],
[('libs/sub/', b''), ('libs/sub/libc.prx', b'content')]):
with self.subTest(entries=entries):
payload = library_zip(entries)
self.publish(payload, payload)
self.assertEqual(json.loads(self.manifest.read_text(encoding="utf-8"))["tag"], "v1")
def setUp(self):
temporary = tempfile.TemporaryDirectory()
self.addCleanup(temporary.cleanup)
self.output = Path(temporary.name)
self.libraries = self.output / "libraries"
self.libraries.mkdir()
self.manifest = self.output / "release.json"
self.previous = b'{"tag":"previous complete release"}\n'
self.manifest.write_bytes(self.previous)
self.existing = self.libraries / "previous.zip"
self.existing.write_bytes(b"previous archive")
def publish(self, windows, linux):
assets = []
responses = {}
for platform, data in (("windows", windows), ("linux", linux)):
url = f"https://example.invalid/{platform}.zip"
assets.append({"name": f"prx-{platform}-v1.zip", "size": len(data),
"digest": "sha256:" + hashlib.sha256(data).hexdigest(), "browser_download_url": url})
responses[url] = data
release = {"tag_name": "v1", "html_url": "https://example.invalid/v1", "assets": assets}
responses["https://api.github.com/repos/boykopovar/AnyPS5/releases/latest"] = json.dumps(release).encode()
with patch.object(build_web, "request", side_effect=lambda url: io.BytesIO(responses[url])), \
contextlib.redirect_stdout(io.StringIO()):
build_web.publish_libraries(self.output)
def test_colliding_paths_do_not_publish_a_release(self):
valid = library_zip([("libs/libc.prx", b"valid archive")])
for first, second in (("libs/libc.prx", "libs/libc.prx"),
("libs/libc.prx", "libs/LIBC.prx"),
("libs/extra/", "libs/EXTRA")):
with self.subTest(first=first, second=second):
invalid = library_zip([(first, b"" if first.endswith("/") else b"first"), (second, b"second")])
with self.assertRaisesRegex(ValueError, "Duplicate library archive path"):
self.publish(invalid, valid)
self.assertEqual(self.manifest.read_bytes(), self.previous)
self.assertEqual(self.existing.read_bytes(), b"previous archive")
self.assertEqual(list(self.libraries.iterdir()), [self.existing])
def test_collision_in_second_platform_preserves_previous_manifest(self):
valid = library_zip([("libs/libc.prx", b"valid archive")])
invalid = library_zip([("libs/libc.prx", b"first"), ("libs/LIBC.prx", b"second")])
with self.assertRaisesRegex(ValueError, "Duplicate library archive path"):
self.publish(valid, invalid)
self.assertEqual(self.manifest.read_bytes(), self.previous)
self.assertEqual(self.existing.read_bytes(), b"previous archive")
self.assertFalse(any(file.suffix == ".download" for file in self.libraries.iterdir()))
def test_distinct_full_paths_publish_both_platforms(self):
payload = library_zip([("libs/", b""), ("libs/libc.prx", b"first"), ("libs/extra/libc.prx", b"second")])
self.publish(payload, payload)
manifest = json.loads(self.manifest.read_text(encoding="utf-8"))
self.assertEqual(manifest["tag"], "v1")
self.assertEqual(set(manifest["platforms"]), {"windows", "linux"})
for platform, asset in manifest["platforms"].items():
digest = hashlib.sha256(payload).hexdigest()
self.assertEqual(asset, {"path": f"libraries/{platform}-{digest}.zip", "size": len(payload),
"expandedBytes": 11, "sha256": digest})
self.assertEqual((self.output / asset["path"]).read_bytes(), payload)
self.assertEqual(self.existing.read_bytes(), b"previous archive")
self.assertFalse(any(file.suffix == ".download" for file in self.libraries.iterdir()))
class BuildWebCliTests(unittest.TestCase):
def test_missing_relinker_wasm_reports_error_without_traceback(self):
with tempfile.TemporaryDirectory() as directory:
build = Path(directory)
result = subprocess.run([sys.executable, str(Path(build_web.__file__)), "--build", str(build)],
capture_output=True, text=True,
env={**os.environ, "PYTHONPATH": str(Path(__file__).resolve().parents[1])})
self.assertEqual(result.returncode, 2)
self.assertIn("Build relinker_wasm first: missing", result.stderr)
self.assertNotIn("Traceback", result.stderr)
def test_main_with_explicit_argv(self):
with tempfile.TemporaryDirectory() as directory:
build = Path(directory)
native = build / "web" / "native"
native.mkdir(parents=True)
(native / "relinker_wasm.mjs").touch()
(native / "relinker_wasm.wasm").touch()
with contextlib.redirect_stdout(io.StringIO()):
code = build_web.main(["--build", str(build)])
self.assertEqual(code, 0)
def test_main_reconfigures_streams(self):
class Stream:
def __init__(self):
self.encoding = None
self.errors = None
def reconfigure(self, encoding=None, errors=None):
self.encoding = encoding
self.errors = errors
def write(self, text):
pass
def flush(self):
pass
out, err = Stream(), Stream()
with patch.object(build_web.sys, "stdout", out), patch.object(build_web.sys, "stderr", err):
try:
build_web.main([])
except SystemExit:
pass
self.assertEqual(out.encoding, "utf-8")
self.assertEqual(out.errors, "backslashreplace")
self.assertEqual(err.encoding, "utf-8")
self.assertEqual(err.errors, "backslashreplace")
if __name__ == "__main__":
unittest.main()