diff --git a/install.ps1 b/install.ps1 index d321dfe7b..19035cf81 100644 --- a/install.ps1 +++ b/install.ps1 @@ -16,7 +16,7 @@ # and Invoke-WebRequest throws on a cut transfer before the hash is read: # the site's build rewrites this line there (scripts/sitebootstrap.py). $mismatchAdvice = "The download was most likely cut short: try again. Within minutes of an update of this project the two files can come from caches of different ages. Waiting five minutes settles that." - $defaultInstallSha256 = "f0410caadbcc354efc86038decb95da63478ddf018cad03ec09e41ce2b5e4d63" + $defaultInstallSha256 = "0aacd5847f927fe058de40f34f09ba4676acafe9d3e3d6c4228e46a474ed8744" $maximumInstallerBytes = 2MB $installerExit = 0 $installer = if ($PSScriptRoot) { Join-Path $PSScriptRoot "install.py" } else { $null } diff --git a/install.py b/install.py index 318048d69..e9d697b78 100644 --- a/install.py +++ b/install.py @@ -138,8 +138,10 @@ OMISSION_REASONS = { # says, and as the installer words it. Firmware carries no category. GAME_DATA = "game_data" CATEGORY_LABELS = {GAME_DATA: "game data"} -_SHA1_RE = re.compile(r"^[0-9a-fA-F]{40}$") -_SHA256_RE = re.compile(r"^[0-9a-fA-F]{64}$") +# The manifest writes its digests in lowercase, as hashlib gives them, and +# every comparison reads them as written. +_SHA1_RE = re.compile(r"^[0-9a-f]{40}$") +_SHA256_RE = re.compile(r"^[0-9a-f]{64}$") # The region vocabulary of scripts/region.py, which this file cannot import. # A test holds the two copies equal. @@ -3592,9 +3594,6 @@ def _check_entry( return size, largest -_SHA1_RE = re.compile(r"^[0-9a-f]{40}$") - - def _check_data_directory(entry: object, where: str) -> None: """A data directory: its source archive, its mapping, its file list.""" if not isinstance(entry, dict): @@ -3631,7 +3630,7 @@ def _check_data_directory(entry: object, where: str) -> None: if ( not isinstance(meta, dict) or not isinstance(meta.get("sha1"), str) - or not _SHA1_RE.match(meta["sha1"]) + or not _SHA1_RE.fullmatch(meta["sha1"]) or not isinstance(meta.get("size"), int) or not 0 <= meta["size"] <= MAX_DOWNLOAD_SIZE ): diff --git a/install.sh b/install.sh index ebf1fc626..d5eaaea86 100755 --- a/install.sh +++ b/install.sh @@ -23,7 +23,7 @@ DEFAULT_INSTALL_URL="https://raw.githubusercontent.com/Abdess/retrobios/main/ins # curl or wget stop on a cut transfer before the hash is read: the site's # build rewrites this line there (scripts/sitebootstrap.py). MISMATCH_ADVICE="The download was most likely cut short: try again. Within minutes of an update of this project the two files can come from caches of different ages. Waiting five minutes settles that." -DEFAULT_INSTALL_SHA256="f0410caadbcc354efc86038decb95da63478ddf018cad03ec09e41ce2b5e4d63" +DEFAULT_INSTALL_SHA256="0aacd5847f927fe058de40f34f09ba4676acafe9d3e3d6c4228e46a474ed8744" MAX_INSTALLER_BYTES=2097152 cleanup() { diff --git a/install/launchers/index.json b/install/launchers/index.json index 5e538378d..300728f9c 100644 --- a/install/launchers/index.json +++ b/install/launchers/index.json @@ -27,8 +27,8 @@ { "dest": "roms/ports/retrobios/install.py", "repo_path": "install.py", - "size": 1089187, - "sha256": "f0410caadbcc354efc86038decb95da63478ddf018cad03ec09e41ce2b5e4d63", + "size": 1089259, + "sha256": "0aacd5847f927fe058de40f34f09ba4676acafe9d3e3d6c4228e46a474ed8744", "executable": false }, { @@ -71,8 +71,8 @@ "muos": { "archive": { "dest": "ARCHIVE/RetroBIOS.muxapp", - "size": 1142814, - "sha256": "090c40f23a9c087641c9835a79a0d4181fc702865ce20e5115fe5a7ca72a31fc" + "size": 1142886, + "sha256": "4068768b8a88f1caa5238a8f9b5f0a0a34c1a6acab3577b54075c4163ca37ca6" }, "members": [ { @@ -92,8 +92,8 @@ { "name": "RetroBIOS/install.py", "repo_path": "install.py", - "size": 1089187, - "sha256": "f0410caadbcc354efc86038decb95da63478ddf018cad03ec09e41ce2b5e4d63", + "size": 1089259, + "sha256": "0aacd5847f927fe058de40f34f09ba4676acafe9d3e3d6c4228e46a474ed8744", "executable": false }, { @@ -166,8 +166,8 @@ { "dest": ".config/modules/retrobios/install.py", "repo_path": "install.py", - "size": 1089187, - "sha256": "f0410caadbcc354efc86038decb95da63478ddf018cad03ec09e41ce2b5e4d63", + "size": 1089259, + "sha256": "0aacd5847f927fe058de40f34f09ba4676acafe9d3e3d6c4228e46a474ed8744", "executable": false }, { diff --git a/tests/test_install_cores.py b/tests/test_install_cores.py index 9c58ba3a5..55fcbfbf0 100644 --- a/tests/test_install_cores.py +++ b/tests/test_install_cores.py @@ -192,6 +192,29 @@ class TheBoundaryBoundsAlternatives(unittest.TestCase): install._validate_manifest(manifest, "retroarch") +class OneDigestRuleAtTheBoundary(unittest.TestCase): + """install.py defined _SHA1_RE twice: the second, lowercase only, + replaced the first everywhere, so an uppercase sha1 was refused where + an uppercase sha256 passed. Every comparison reads the digest as + hashlib writes it, so both refuse uppercase, data trees included.""" + + def _manifest(self, **digests: str) -> dict: + entry = {"dest": "a.bin", "size": 1, "repo_path": "bios/a.bin", **digests} + return {"manifest_version": 2, "platform": "retroarch", "files": [entry]} + + def test_sha1_and_sha256_follow_the_same_rule(self): + install._validate_manifest(self._manifest(sha1="a" * 40), "retroarch") + install._validate_manifest(self._manifest(sha256="a" * 64), "retroarch") + for digests in ({"sha1": "A" * 40}, {"sha256": "A" * 64}): + with self.subTest(digests=digests), self.assertRaises(ValueError): + install._validate_manifest(self._manifest(**digests), "retroarch") + + def test_one_definition_each(self): + source = (REPO_ROOT / "install.py").read_text(encoding="utf-8") + for name in ("_SHA1_RE", "_SHA256_RE"): + self.assertEqual(source.count(f"\n{name} = "), 1, name) + + class APlatformOmissionKeepsItsClaim(unittest.TestCase): """A file the platform declares and a core reads, absent from the collection, was recorded under the core alone: RetroPie's dsi_nand.bin