diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 7c9fc6a..a61a4cf 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -21,3 +21,6 @@ jobs: - name: Update tests outputs run: python3 tests/check_correct_sum.py + + - name: Test exceptions + run: python3 tests/exception_test.py diff --git a/CHANGELOG.md b/CHANGELOG.md index d1ac65c..21d54a0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,22 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +## [1.2.0] - 2024-01-11 + +### Added + +- `CICKind.get_entrypoint`: Returns the entrypoint address that would be used + on runtime. +- `CICKind.calculate_checksum`: Convinience method that wraps + `checksum::calculate_checksum`. +- Python bindings: + - Expose `Ipl3ChecksumError` to Python as a new exception for each error of + the enum. Refer to `ipl3checksum.exceptions`. + +### Changed + +- Rewrite the checksum algorithm for readability and simplicity. + ## [1.1.1] - 2023-12-23 ### Fixed @@ -14,7 +30,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Python bindings: - Fix `detectCIC` and `detect_cic_raw` functions not accepting `bytearray` objects. -- Fix some typos +- Fix some typos. ## [1.1.0] - 2023-12-22 @@ -62,6 +78,7 @@ version of the library. - Initial relase [unreleased]: https://github.com/Decompollaborate/ipl3checksum/compare/main...develop +[1.2.0]: https://github.com/Decompollaborate/ipl3checksum/compare/1.1.1...1.2.0 [1.1.1]: https://github.com/Decompollaborate/ipl3checksum/compare/1.1.0...1.1.1 [1.1.0]: https://github.com/Decompollaborate/ipl3checksum/compare/1.0.1...1.1.0 [1.0.1]: https://github.com/Decompollaborate/ipl3checksum/compare/1.0.0...1.0.1 diff --git a/Cargo.lock b/Cargo.lock index ab03454..8a369fc 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -34,7 +34,7 @@ checksum = "1e186cfbae8084e513daff4240b4797e342f988cecda4fb6c939150f96315fd8" [[package]] name = "ipl3checksum" -version = "1.1.1" +version = "1.2.0" dependencies = [ "md5", "pyo3", @@ -43,9 +43,9 @@ dependencies = [ [[package]] name = "libc" -version = "0.2.151" +version = "0.2.152" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "302d7ab3130588088d277783b1e2d2e10c9e9e4a16dd9050e6ec93fb3e7048f4" +checksum = "13e3bf6590cbc649f4d1a3eefc9d5d6eb746f5200ffb04e5e142700b8faa56e7" [[package]] name = "lock_api" @@ -103,18 +103,18 @@ dependencies = [ [[package]] name = "proc-macro2" -version = "1.0.71" +version = "1.0.76" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "75cb1540fadbd5b8fbccc4dddad2734eba435053f725621c070711a14bb5f4b8" +checksum = "95fc56cda0b5c3325f5fbbd7ff9fda9e02bb00bb3dac51252d2f1bfa1cb8cc8c" dependencies = [ "unicode-ident", ] [[package]] name = "pyo3" -version = "0.20.0" +version = "0.20.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "04e8453b658fe480c3e70c8ed4e3d3ec33eb74988bd186561b0cc66b85c3bc4b" +checksum = "9a89dc7a5850d0e983be1ec2a463a171d20990487c3cfcd68b5363f1ee3d6fe0" dependencies = [ "cfg-if", "indoc", @@ -129,9 +129,9 @@ dependencies = [ [[package]] name = "pyo3-build-config" -version = "0.20.0" +version = "0.20.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a96fe70b176a89cff78f2fa7b3c930081e163d5379b4dcdf993e3ae29ca662e5" +checksum = "07426f0d8fe5a601f26293f300afd1a7b1ed5e78b2a705870c5f30893c5163be" dependencies = [ "once_cell", "target-lexicon", @@ -139,9 +139,9 @@ dependencies = [ [[package]] name = "pyo3-ffi" -version = "0.20.0" +version = "0.20.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "214929900fd25e6604661ed9cf349727c8920d47deff196c4e28165a6ef2a96b" +checksum = "dbb7dec17e17766b46bca4f1a4215a85006b4c2ecde122076c562dd058da6cf1" dependencies = [ "libc", "pyo3-build-config", @@ -149,9 +149,9 @@ dependencies = [ [[package]] name = "pyo3-macros" -version = "0.20.0" +version = "0.20.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dac53072f717aa1bfa4db832b39de8c875b7c7af4f4a6fe93cdbf9264cf8383b" +checksum = "05f738b4e40d50b5711957f142878cfa0f28e054aa0ebdfc3fd137a843f74ed3" dependencies = [ "proc-macro2", "pyo3-macros-backend", @@ -161,9 +161,9 @@ dependencies = [ [[package]] name = "pyo3-macros-backend" -version = "0.20.0" +version = "0.20.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7774b5a8282bd4f25f803b1f0d945120be959a36c72e08e7cd031c792fdfd424" +checksum = "0fc910d4851847827daf9d6cdd4a823fbdaab5b8818325c5e97a86da79e8881f" dependencies = [ "heck", "proc-macro2", @@ -173,9 +173,9 @@ dependencies = [ [[package]] name = "quote" -version = "1.0.33" +version = "1.0.35" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5267fca4496028628a95160fc423a33e8b2e6af8a5302579e322e4b520293cae" +checksum = "291ec9ab5efd934aaf503a6466c5d5251535d108ee747472c3977cc5acc868ef" dependencies = [ "proc-macro2", ] @@ -203,9 +203,9 @@ checksum = "4dccd0940a2dcdf68d092b8cbab7dc0ad8fa938bf95787e1b916b0e3d0e8e970" [[package]] name = "syn" -version = "2.0.42" +version = "2.0.48" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5b7d0a2c048d661a1a59fcd7355baa232f7ed34e0ee4df2eef3c1c1c0d3852d8" +checksum = "0f3531638e407dfc0814761abb7c00a5b54992b849452a0646b7f65c9f770f3f" dependencies = [ "proc-macro2", "quote", @@ -214,24 +214,24 @@ dependencies = [ [[package]] name = "target-lexicon" -version = "0.12.12" +version = "0.12.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "14c39fd04924ca3a864207c66fc2cd7d22d7c016007f9ce846cbb9326331930a" +checksum = "69758bda2e78f098e4ccb393021a0963bb3442eac05f135c30f61b7370bbafae" [[package]] name = "thiserror" -version = "1.0.51" +version = "1.0.56" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f11c217e1416d6f036b870f14e0413d480dbf28edbee1f877abaf0206af43bb7" +checksum = "d54378c645627613241d077a3a79db965db602882668f9136ac42af9ecb730ad" dependencies = [ "thiserror-impl", ] [[package]] name = "thiserror-impl" -version = "1.0.51" +version = "1.0.56" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "01742297787513b79cf8e29d1056ede1313e2420b7b3b15d0a768b4921f549df" +checksum = "fa0faa943b50f3db30a20aa7e265dbc66076993efed8463e8de414e5d06d3471" dependencies = [ "proc-macro2", "quote", diff --git a/Cargo.toml b/Cargo.toml index c0a0e7c..d434698 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,10 +1,10 @@ -# SPDX-FileCopyrightText: © 2023 Decompollaborate +# SPDX-FileCopyrightText: © 2023-2024 Decompollaborate # SPDX-License-Identifier: MIT [package] name = "ipl3checksum" # Version should be synced with src/ipl3checksum/__init__.py, pyproject.toml and src/rs/version.rs -version = "1.1.1" +version = "1.2.0" edition = "2021" description = "Library to calculate the IPL3 checksum for N64 ROMs" repository = "https://github.com/decompollaborate/ipl3checksum" @@ -17,8 +17,8 @@ crate-type = ["lib", "staticlib", "cdylib"] [dependencies] md5 = "0.7.0" -pyo3 = { version="0.20.0", features = ["extension-module"], optional = true } -thiserror = "1.0.51" +pyo3 = { version="0.20.2", features = ["extension-module"], optional = true } +thiserror = "1.0.56" [features] c_bindings = [] diff --git a/bindings/c/include/ipl3checksum/cickinds.h b/bindings/c/include/ipl3checksum/cickinds.h index bed923c..6f4d7f7 100644 --- a/bindings/c/include/ipl3checksum/cickinds.h +++ b/bindings/c/include/ipl3checksum/cickinds.h @@ -29,6 +29,8 @@ uint32_t ipl3checksum_cickind_get_seed(Ipl3Checksum_CICKind self); uint32_t ipl3checksum_cickind_get_magic(Ipl3Checksum_CICKind self); +uint32_t ipl3checksum_cickind_get_entrypoint(Ipl3Checksum_CICKind self, uint32_t header_entrypoint); + /** * Returns the md5 hash for the specified CIC kind. * diff --git a/bindings/c/tests/test_checksum.c b/bindings/c/tests/test_checksum.c index c98704a..12a5795 100644 --- a/bindings/c/tests/test_checksum.c +++ b/bindings/c/tests/test_checksum.c @@ -1,4 +1,4 @@ -/* SPDX-FileCopyrightText: © 2023 Decompollaborate */ +/* SPDX-FileCopyrightText: © 2023-2024 Decompollaborate */ /* SPDX-License-Identifier: MIT */ #include "ipl3checksum.h" diff --git a/bindings/c/tests/test_checksum_autodetect.c b/bindings/c/tests/test_checksum_autodetect.c index 5a4d121..6464f28 100644 --- a/bindings/c/tests/test_checksum_autodetect.c +++ b/bindings/c/tests/test_checksum_autodetect.c @@ -1,4 +1,4 @@ -/* SPDX-FileCopyrightText: © 2023 Decompollaborate */ +/* SPDX-FileCopyrightText: © 2023-2024 Decompollaborate */ /* SPDX-License-Identifier: MIT */ #include "ipl3checksum.h" diff --git a/bindings/c/tests/test_detect.c b/bindings/c/tests/test_detect.c index 6027436..1239e8b 100644 --- a/bindings/c/tests/test_detect.c +++ b/bindings/c/tests/test_detect.c @@ -1,4 +1,4 @@ -/* SPDX-FileCopyrightText: © 2023 Decompollaborate */ +/* SPDX-FileCopyrightText: © 2023-2024 Decompollaborate */ /* SPDX-License-Identifier: MIT */ #include "ipl3checksum.h" diff --git a/bindings/c/tests/utils.c b/bindings/c/tests/utils.c index dfeabc5..59e5b4f 100644 --- a/bindings/c/tests/utils.c +++ b/bindings/c/tests/utils.c @@ -1,4 +1,4 @@ -/* SPDX-FileCopyrightText: © 2023 Decompollaborate */ +/* SPDX-FileCopyrightText: © 2023-2024 Decompollaborate */ /* SPDX-License-Identifier: MIT */ #include "utils.h" diff --git a/bindings/c/tests/utils.h b/bindings/c/tests/utils.h index b5486d8..32e2f87 100644 --- a/bindings/c/tests/utils.h +++ b/bindings/c/tests/utils.h @@ -1,4 +1,4 @@ -/* SPDX-FileCopyrightText: © 2023 Decompollaborate */ +/* SPDX-FileCopyrightText: © 2023-2024 Decompollaborate */ /* SPDX-License-Identifier: MIT */ #ifndef TESTS_UTILS_H diff --git a/pyproject.toml b/pyproject.toml index da428a5..6248e47 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,25 +1,42 @@ -# SPDX-FileCopyrightText: © 2023 Decompollaborate +# SPDX-FileCopyrightText: © 2023-2024 Decompollaborate # SPDX-License-Identifier: MIT [project] name = "ipl3checksum" # Version should be synced with src/ipl3checksum/__init__.py, Cargo.toml and src/rs/version.rs -version = "1.1.1" +version = "1.2.0" description = "Library to calculate the IPL3 checksum for N64 ROMs" readme = "README.md" requires-python = ">=3.7" +license = {file = "LICENSE"} +keywords = ["IPL3", "CIC", "checksum", "N64", "Nintendo 64"] authors = [ { name="Anghelo Carvajal", email="angheloalf95@gmail.com" }, ] classifiers = [ "Programming Language :: Rust", - "Programming Language :: Python :: Implementation :: CPython", - "Programming Language :: Python :: Implementation :: PyPy", + "Programming Language :: Python :: 3", + + "Development Status :: 5 - Production/Stable", + + "Intended Audience :: Developers", + + "License :: OSI Approved :: MIT License", + + "Operating System :: POSIX :: Linux", + "Operating System :: Microsoft :: Windows", + "Operating System :: MacOS", + + "Topic :: Software Development :: Libraries", + "Topic :: Software Development :: Libraries :: Python Modules", + + "Typing :: Typed", ] [project.urls] -"Homepage" = "https://github.com/decompollaborate/ipl3checksum" -"Bug Tracker" = "https://github.com/decompollaborate/ipl3checksum/issues" +Repository = "https://github.com/Decompollaborate/ipl3checksum" +Issues = "https://github.com/Decompollaborate/ipl3checksum/issues" +Changelog = "https://github.com/Decompollaborate/ipl3checksum/blob/master/CHANGELOG.md" [build-system] requires = ["maturin>=1.2,<2.0"] diff --git a/src/ipl3checksum/__init__.py b/src/ipl3checksum/__init__.py index 23436fe..372d033 100644 --- a/src/ipl3checksum/__init__.py +++ b/src/ipl3checksum/__init__.py @@ -1,12 +1,12 @@ #!/usr/bin/env python3 -# SPDX-FileCopyrightText: © 2023 Decompollaborate +# SPDX-FileCopyrightText: © 2023-2024 Decompollaborate # SPDX-License-Identifier: MIT from __future__ import annotations # Version should be synced with pyproject.toml, Cargo.toml and src/rs/version.rs -__version_info__: tuple[int, int, int] = (1, 1, 1) +__version_info__: tuple[int, int, int] = (1, 2, 0) __version__ = ".".join(map(str, __version_info__)) __author__ = "Decompollaborate" diff --git a/src/ipl3checksum/__main__.py b/src/ipl3checksum/__main__.py index 5ab0ea6..d27d6a4 100644 --- a/src/ipl3checksum/__main__.py +++ b/src/ipl3checksum/__main__.py @@ -1,6 +1,6 @@ #!/usr/bin/env python3 -# SPDX-FileCopyrightText: © 2023 Decompollaborate +# SPDX-FileCopyrightText: © 2023-2024 Decompollaborate # SPDX-License-Identifier: MIT from __future__ import annotations diff --git a/src/ipl3checksum/checksum.pyi b/src/ipl3checksum/checksum.pyi index c709541..6990043 100644 --- a/src/ipl3checksum/checksum.pyi +++ b/src/ipl3checksum/checksum.pyi @@ -1,6 +1,6 @@ #!/usr/bin/env python3 -# SPDX-FileCopyrightText: © 2023 Decompollaborate +# SPDX-FileCopyrightText: © 2023-2024 Decompollaborate # SPDX-License-Identifier: MIT from __future__ import annotations diff --git a/src/ipl3checksum/cickinds.pyi b/src/ipl3checksum/cickinds.pyi index 63c3358..a0caf40 100644 --- a/src/ipl3checksum/cickinds.pyi +++ b/src/ipl3checksum/cickinds.pyi @@ -1,6 +1,6 @@ #!/usr/bin/env python3 -# SPDX-FileCopyrightText: © 2023 Decompollaborate +# SPDX-FileCopyrightText: © 2023-2024 Decompollaborate # SPDX-License-Identifier: MIT from __future__ import annotations @@ -20,6 +20,7 @@ class CICKind(): def getSeed(self) -> int: """ Seed value set by the PIF ROM before the CPU (and the IPL3) is executed. + https://n64brew.dev/wiki/PIF-NUS#IPL3_checksum_algorithm """ @@ -28,6 +29,13 @@ class CICKind(): Magic value hardcoded inside the IPL3 itself """ + def getEntrypoint(self, header_entrypoint: int) -> int: + """ + Calculates the actual entrypoint address based on the entrypoint specified on the header. + + CIC 7102 is a notable case since its IPL3 hardcodes it, ignoring the entrypoint from the header. + """ + def getHashMd5(self) -> str: """ Expected md5 hash of the IPL3 blob @@ -52,3 +60,14 @@ class CICKind(): @staticmethod def fromValue(value: int) -> CICKind|None: ... + + def calculateChecksum(self, romBytes: bytes) -> tuple[int, int]: + """Calculates the checksum required by an official CIC of a N64 ROM. + + Args: + romBytes (bytes): The bytes of the N64 ROM in big endian format. It must have a minimum size of 0x101000 bytes. + + Returns: + tuple[int, int]: If no error happens then the calculated checksum is returned, stored as a tuple + containing two 32-bits words. If an errors occurs an exception will be raised (see ipl3checksum.exceptions). + """ diff --git a/src/ipl3checksum/detect.pyi b/src/ipl3checksum/detect.pyi index b0ab4ba..13073d7 100644 --- a/src/ipl3checksum/detect.pyi +++ b/src/ipl3checksum/detect.pyi @@ -1,6 +1,6 @@ #!/usr/bin/env python3 -# SPDX-FileCopyrightText: © 2023 Decompollaborate +# SPDX-FileCopyrightText: © 2023-2024 Decompollaborate # SPDX-License-Identifier: MIT from __future__ import annotations diff --git a/src/ipl3checksum/exceptions/exceptions.pyi b/src/ipl3checksum/exceptions/exceptions.pyi new file mode 100644 index 0000000..4642bf9 --- /dev/null +++ b/src/ipl3checksum/exceptions/exceptions.pyi @@ -0,0 +1,55 @@ +#!/usr/bin/env python3 + +# SPDX-FileCopyrightText: © 2024 Decompollaborate +# SPDX-License-Identifier: MIT + +from __future__ import annotations + + +class Ipl3ChecksumError(RuntimeError): + """ + Base exception for all the exceptions raised by this library. + """ + + +class UnalignedRead(Ipl3ChecksumError): + """ + An unaligned read happened. + + (This is probably a library bug, please report me). + """ + +class ByteConversion(Ipl3ChecksumError): + """ + Failed to convert bytes to words. + + (This is probably a library bug, please report me). + """ + +class OutOfBounds(Ipl3ChecksumError): + """ + Tried to access data out of bounds. + + (This is probably a library bug, please report me). + """ + +class BufferNotBigEnough(Ipl3ChecksumError): + """ + The input byte buffer is not big enough. + + The buffer can be larger than the expected size. + + The error runtime string specifies how big the buffer was expected to be. + """ + +class BufferSizeIsWrong(Ipl3ChecksumError): + """ + The input byte buffer didn't have the exact expected size. + + The error runtime string specifies the expected size. + """ + +class UnableToDetectCIC(Ipl3ChecksumError): + """ + Unable to detect CIC variant + """ diff --git a/src/ipl3checksum/frontends/__init__.py b/src/ipl3checksum/frontends/__init__.py index d6fa356..69ecd55 100644 --- a/src/ipl3checksum/frontends/__init__.py +++ b/src/ipl3checksum/frontends/__init__.py @@ -1,6 +1,6 @@ #!/usr/bin/env python3 -# SPDX-FileCopyrightText: © 2023 Decompollaborate +# SPDX-FileCopyrightText: © 2023-2024 Decompollaborate # SPDX-License-Identifier: MIT from __future__ import annotations diff --git a/src/ipl3checksum/frontends/check.py b/src/ipl3checksum/frontends/check.py index 3c1facc..e6e157d 100644 --- a/src/ipl3checksum/frontends/check.py +++ b/src/ipl3checksum/frontends/check.py @@ -1,6 +1,6 @@ #!/usr/bin/env python3 -# SPDX-FileCopyrightText: © 2023 Decompollaborate +# SPDX-FileCopyrightText: © 2023-2024 Decompollaborate # SPDX-License-Identifier: MIT from __future__ import annotations diff --git a/src/ipl3checksum/frontends/detect_cic.py b/src/ipl3checksum/frontends/detect_cic.py index 7330133..49e7565 100644 --- a/src/ipl3checksum/frontends/detect_cic.py +++ b/src/ipl3checksum/frontends/detect_cic.py @@ -1,6 +1,6 @@ #!/usr/bin/env python3 -# SPDX-FileCopyrightText: © 2023 Decompollaborate +# SPDX-FileCopyrightText: © 2023-2024 Decompollaborate # SPDX-License-Identifier: MIT from __future__ import annotations diff --git a/src/ipl3checksum/frontends/sum.py b/src/ipl3checksum/frontends/sum.py index 6d4f557..eca1a0f 100644 --- a/src/ipl3checksum/frontends/sum.py +++ b/src/ipl3checksum/frontends/sum.py @@ -1,6 +1,6 @@ #!/usr/bin/env python3 -# SPDX-FileCopyrightText: © 2023 Decompollaborate +# SPDX-FileCopyrightText: © 2023-2024 Decompollaborate # SPDX-License-Identifier: MIT from __future__ import annotations diff --git a/src/ipl3checksum/ipl3checksum.pyi b/src/ipl3checksum/ipl3checksum.pyi index 9dda0b8..bd91aa8 100644 --- a/src/ipl3checksum/ipl3checksum.pyi +++ b/src/ipl3checksum/ipl3checksum.pyi @@ -1,6 +1,6 @@ #!/usr/bin/env python3 -# SPDX-FileCopyrightText: © 2023 Decompollaborate +# SPDX-FileCopyrightText: © 2023-2024 Decompollaborate # SPDX-License-Identifier: MIT from __future__ import annotations @@ -12,3 +12,5 @@ from .checksum import calculateChecksumAutodetect as calculateChecksumAutodetect from .detect import detectCIC as detectCIC from .detect import detectCICRaw as detectCICRaw + +from .exceptions import exceptions as exceptions diff --git a/src/rs/checksum.rs b/src/rs/checksum.rs index ab5c1d1..c419b85 100644 --- a/src/rs/checksum.rs +++ b/src/rs/checksum.rs @@ -1,13 +1,17 @@ -/* SPDX-FileCopyrightText: © 2023 Decompollaborate */ +/* SPDX-FileCopyrightText: © 2023-2024 Decompollaborate */ /* SPDX-License-Identifier: MIT */ use crate::cickinds::CICKind; use crate::{detect, error::Ipl3ChecksumError, utils}; -fn read_word_from_ram(rom_words: &[u32], entrypoint_ram: u32, ram_addr: u32) -> u32 { - rom_words[((ram_addr - entrypoint_ram + 0x1000) / 4) as usize] +fn get_entrypoint_addr(rom_bytes: &[u8], kind: CICKind) -> Result { + let entrypoint_addr: u32 = utils::read_u32(rom_bytes, 8)?; + + Ok(kind.get_entrypoint(entrypoint_addr)) } +const HEADER_IPL3_SIZE: usize = 0x1000; + /// Calculates the checksum required by an official CIC of a N64 ROM. /// /// ## Arguments @@ -18,7 +22,7 @@ fn read_word_from_ram(rom_words: &[u32], entrypoint_ram: u32, ram_addr: u32) -> /// ## Return /// /// * If no error happens then the calculated checksum is returned, stored as a tuple -/// containing two 32-bits words. Otherwise, `None` is returned. +/// containing two 32-bits words. /// /// ## Examples /// @@ -33,45 +37,10 @@ pub fn calculate_checksum( rom_bytes: &[u8], kind: CICKind, ) -> Result<(u32, u32), Ipl3ChecksumError> { - if rom_bytes.len() < 0x101000 { - return Err(Ipl3ChecksumError::BufferNotBigEnough { - buffer_len: rom_bytes.len(), - expected_len: 0x101000, - }); - } - let seed = kind.get_seed(); let magic = kind.get_magic(); - let mut s6 = seed; - - let mut a0 = utils::read_u32(rom_bytes, 8)?; - if kind == CICKind::CIC_X103 || kind == CICKind::CIC_5101 { - a0 = a0.wrapping_sub(0x100000); - } - if kind == CICKind::CIC_X106 { - a0 = a0.wrapping_sub(0x200000); - } - let entrypoint_ram = a0; - - let mut at = magic; - let lo = s6.wrapping_mul(at); - - if kind == CICKind::CIC_X105 { - s6 = 0xA0000200; - } - - let mut ra: u32 = 0x100000; - - let mut t0: u32 = 0; - - let mut t1: u32 = a0; - - let t5: u32 = 0x20; - - //let mut v0 = utils.u32(lo); - let mut v0 = lo; - v0 += 1; + let v0 = seed.wrapping_mul(magic).wrapping_add(1); let mut a3 = v0; let mut t2 = v0; @@ -80,141 +49,83 @@ pub fn calculate_checksum( let mut a2 = v0; let mut t4 = v0; - #[allow(clippy::single_match)] - match kind { - CICKind::CIC_5101 => { - if a0 == 0x80000400 { - ra = 0x3FE000; - if rom_bytes.len() < 0x3FE000 + 0x1000 { - return Err(Ipl3ChecksumError::BufferNotBigEnough { - buffer_len: rom_bytes.len(), - expected_len: 0x3FE000 + 0x1000, - }); - } - } - } - _ => (), - } - - let rom_words = utils::read_u32_vec(rom_bytes, 0, (ra as usize + 0x1000) / 4)?; - - // poor man's do while - // LA40005F0_loop - let mut loop_variable = true; - while loop_variable { - // v0 = *t1 - v0 = read_word_from_ram(&rom_words, entrypoint_ram, t1); + // Get how many bytes of the ROM (passed IPL3) to check + let bytes_to_check: u32 = + // IPL3 5101 checks almost 4 times the normal amount depending on the entrypoint + if (kind == CICKind::CIC_5101) && (get_entrypoint_addr(rom_bytes, kind)? == 0x80000400) { + 0x3FE000 // ~ 3.992 MiB + } else { + 0x100000 + }; - //v1 = utils.u32(a3 + v0); - let mut v1 = a3.wrapping_add(v0); + // Error if the ROM is not big enough + if rom_bytes.len() < bytes_to_check as usize + HEADER_IPL3_SIZE { + return Err(Ipl3ChecksumError::BufferNotBigEnough { + buffer_len: rom_bytes.len(), + expected_len: bytes_to_check as usize + HEADER_IPL3_SIZE, + }); + } - //at = utils.u32(v1) < utils.u32(a3); - at = if v1 < a3 { 1 } else { 0 }; + let rom_words = utils::read_u32_vec( + rom_bytes, + 0, + (bytes_to_check as usize + HEADER_IPL3_SIZE) / 4, + )?; - let a1 = v1; - // if (at == 0) goto LA4000608; + let words_to_check = bytes_to_check.wrapping_div(4) as usize; + for i in 0..words_to_check { + let word = rom_words[i + (HEADER_IPL3_SIZE / 4)]; - if at != 0 { - //t2 = utils.u32(t2 + 0x1) + let a1 = a3.wrapping_add(word); + if a1 < a3 { t2 = t2.wrapping_add(0x1); } - - // LA4000608 - v1 = v0 & 0x1F; - //t7 = utils.u32(t5 - v1) - let t7: u32 = t5.wrapping_sub(v1); - - //let t8 = utils.u32(v0 >> t7) - //let t6 = utils.u32(v0 << v1) - let t8 = v0.wrapping_shr(t7); - let t6 = v0.wrapping_shl(v1); - - a0 = t6 | t8; - // at = utils.u32(a2) < utils.u32(v0); - at = if a2 < v0 { 1 } else { 0 }; a3 = a1; - t3 ^= v0; - - //s0 = utils.u32(s0 + a0) - s0 = s0.wrapping_add(a0); - // if (at == 0) goto LA400063C; - if at != 0 { - let t9 = a3 ^ v0; + let a0 = word.rotate_left(word & 0x1F); - a2 ^= t9; - // goto LA4000640; + t3 ^= word; - // LA400063C: + s0 = s0.wrapping_add(a0); + if a2 < word { + a2 ^= a3 ^ word; } else { a2 ^= a0; } - // LA4000640: if kind == CICKind::CIC_X105 { // ipl3 6105 copies 0x330 bytes from the ROM's offset 0x000554 (or offset 0x000514 into IPL3) to vram 0xA0000004 - let mut t7 = rom_words[((s6 - 0xA0000004 + 0x000554) / 4) as usize]; - - //t0 = utils.u32(t0 + 0x4); - //s6 = utils.u32(s6 + 0x4); - t0 = t0.wrapping_add(0x4); - s6 = s6.wrapping_add(0x4); - - t7 ^= v0; - - // t4 = utils.u32(t7 + t4); - t4 = t7.wrapping_add(t4); - - t7 = 0xA00002FF; - - // t1 = utils.u32(t1 + 0x4); - t1 = t1.wrapping_add(0x4); + let temp = (i & 0x3F) | 0x80; + let t7 = rom_words[temp + 0x154]; - // s6 = utils.u32(s6 & t7); - s6 &= t7; + t4 = t4.wrapping_add(word ^ t7); } else { - // t0 = utils.u32(t0 + 0x4); - t0 = t0.wrapping_add(0x4); - - let t7 = v0 ^ s0; + t4 = t4.wrapping_add(word ^ s0); + } + } - // t1 = utils.u32(t1 + 0x4); - t1 = t1.wrapping_add(0x4); + match kind { + CICKind::CIC_X103 | CICKind::CIC_5101 => { + let t6 = a3 ^ t2; + a3 = t6.wrapping_add(t3); - // t4 = utils.u32(t7 + t4); - t4 = t7.wrapping_add(t4); + let t8 = s0 ^ a2; + s0 = t8.wrapping_add(t4); } + CICKind::CIC_X106 => { + let t6 = a3.wrapping_mul(t2); + a3 = t6.wrapping_add(t3); - // if (t0 != ra) goto LA40005F0; - if t0 == ra { - loop_variable = false; + let t8 = s0.wrapping_mul(a2); + s0 = t8.wrapping_add(t4); } - } + _ => { + let t6 = a3 ^ t2; + a3 = t6 ^ t3; - if kind == CICKind::CIC_X103 || kind == CICKind::CIC_5101 { - let t6 = a3 ^ t2; - // a3 = utils.u32(t6 + t3); - a3 = t6.wrapping_add(t3); - - let t8 = s0 ^ a2; - // s0 = utils.u32(t8 + t4); - s0 = t8.wrapping_add(t4); - } else if kind == CICKind::CIC_X106 { - /* - let t6 = utils.u32(a3 * t2); - a3 = utils.u32(t6 + t3); - let t8 = utils.u32(s0 * a2); - s0 = utils.u32(t8 + t4); - */ - let t6 = a3.wrapping_mul(t2); - a3 = t6.wrapping_add(t3); - let t8 = s0.wrapping_mul(a2); - s0 = t8.wrapping_add(t4); - } else { - let t6 = a3 ^ t2; - a3 = t6 ^ t3; - let t8 = s0 ^ a2; - s0 = t8 ^ t4; + let t8 = s0 ^ a2; + s0 = t8 ^ t4; + } } Ok((a3, s0)) diff --git a/src/rs/cickinds.rs b/src/rs/cickinds.rs index b8f4e2b..e8c201c 100644 --- a/src/rs/cickinds.rs +++ b/src/rs/cickinds.rs @@ -1,10 +1,10 @@ -/* SPDX-FileCopyrightText: © 2023 Decompollaborate */ +/* SPDX-FileCopyrightText: © 2023-2024 Decompollaborate */ /* SPDX-License-Identifier: MIT */ #[cfg(feature = "python_bindings")] use pyo3::prelude::*; -use crate::Ipl3ChecksumError; +use crate::{checksum, Ipl3ChecksumError}; /* This needs to be in sync with the C equivalent at `bindings/c/include/ipl3checksum/cickinds.h` */ #[cfg_attr(feature = "python_bindings", pyclass(module = "ipl3checksum"))] @@ -27,6 +27,9 @@ pub enum CICKind { } impl CICKind { + /// Seed value set by the PIF ROM before the CPU (and the IPL3) is executed. + /// + /// https://n64brew.dev/wiki/PIF-NUS#IPL3_checksum_algorithm pub fn get_seed(&self) -> u32 { match self { Self::CIC_6101 | Self::CIC_6102_7101 | Self::CIC_7102 => 0x3F, @@ -37,6 +40,7 @@ impl CICKind { } } + /// Magic value hardcoded inside the IPL3 itself pub fn get_magic(&self) -> u32 { match self { Self::CIC_6101 | Self::CIC_6102_7101 | Self::CIC_7102 | Self::CIC_X105 => 0x5D588B65, @@ -44,6 +48,18 @@ impl CICKind { } } + /// Calculates the actual entrypoint address based on the entrypoint specified on the header. + /// + /// CIC 7102 is a notable case since its IPL3 hardcodes it, ignoring the entrypoint from the header. + pub fn get_entrypoint(&self, header_entrypoint: u32) -> u32 { + match self { + CICKind::CIC_7102 => 0x80000480, + CICKind::CIC_X103 | CICKind::CIC_5101 => header_entrypoint.wrapping_sub(0x100000), + CICKind::CIC_X106 => header_entrypoint.wrapping_sub(0x200000), + _ => header_entrypoint, + } + } + pub fn get_hash_md5(&self) -> &'static str { match self { Self::CIC_6101 => "900b4a5b68edb71f4c7ed52acd814fc5", @@ -144,15 +160,47 @@ impl CICKind { _ => Err(Ipl3ChecksumError::UnableToDetectCIC), } } + + /// Calculates the checksum required by an official CIC of a N64 ROM. + /// + /// ## Arguments + /// + /// * `rom_bytes` - The bytes of the N64 ROM in big endian format. It must have a minimum size of 0x101000 bytes. + /// + /// ## Return + /// + /// * If no error happens then the calculated checksum is returned, stored as a tuple + /// containing two 32-bits words. + /// + /// ## Examples + /// + /// ``` + /// use ipl3checksum; + /// let bytes = vec![0; 0x101000]; + /// let kind = ipl3checksum::CICKind::CIC_6102_7101; + /// let checksum = kind.calculate_checksum(&bytes).unwrap(); + /// println!("{:08X} {:08X}", checksum.0, checksum.1); + /// ``` + pub fn calculate_checksum(&self, rom_bytes: &[u8]) -> Result<(u32, u32), Ipl3ChecksumError> { + checksum::calculate_checksum(rom_bytes, *self) + } } #[cfg(feature = "python_bindings")] #[allow(non_snake_case)] mod python_bindings { use pyo3::prelude::*; + use std::borrow::Cow; use crate::Ipl3ChecksumError; + /** + * We use a `Cow` instead of a plain &[u8] the latter only allows Python's + * `bytes` objects, while Cow allows for both `bytes` and `bytearray`. + * This is important because an argument typed as `bytes` allows to pass a + * `bytearray` object too. + */ + #[pymethods] impl super::CICKind { pub fn getSeed(&self) -> u32 { @@ -163,6 +211,10 @@ mod python_bindings { self.get_magic() } + pub fn getEntrypoint(&self, header_entrypoint: u32) -> u32 { + self.get_entrypoint(header_entrypoint) + } + pub fn getHashMd5(&self) -> &str { self.get_hash_md5() } @@ -209,6 +261,13 @@ mod python_bindings { }, } } + + pub fn calculateChecksum( + &self, + rom_bytes: Cow<[u8]>, + ) -> Result<(u32, u32), Ipl3ChecksumError> { + self.calculate_checksum(&rom_bytes) + } } } @@ -226,6 +285,14 @@ mod c_bindings { kind.get_magic() } + #[no_mangle] + pub extern "C" fn ipl3checksum_cickind_get_entrypoint( + kind: CICKind, + header_entrypoint: u32, + ) -> u32 { + kind.get_entrypoint(header_entrypoint) + } + #[no_mangle] pub extern "C" fn ipl3checksum_cickind_get_hash_md5( kind: CICKind, diff --git a/src/rs/detect.rs b/src/rs/detect.rs index 5da5860..2d1d469 100644 --- a/src/rs/detect.rs +++ b/src/rs/detect.rs @@ -1,4 +1,4 @@ -/* SPDX-FileCopyrightText: © 2023 Decompollaborate */ +/* SPDX-FileCopyrightText: © 2023-2024 Decompollaborate */ /* SPDX-License-Identifier: MIT */ use crate::{cickinds::CICKind, error::Ipl3ChecksumError, utils}; diff --git a/src/rs/error.rs b/src/rs/error.rs index f5206bc..7c35d54 100644 --- a/src/rs/error.rs +++ b/src/rs/error.rs @@ -1,11 +1,6 @@ -/* SPDX-FileCopyrightText: © 2023 Decompollaborate */ +/* SPDX-FileCopyrightText: © 2023-2024 Decompollaborate */ /* SPDX-License-Identifier: MIT */ -#[cfg(feature = "python_bindings")] -use pyo3::exceptions::PyRuntimeError; -#[cfg(feature = "python_bindings")] -use pyo3::prelude::*; - /* This needs to be in sync with the C equivalent at `bindings/c/include/ipl3checksum/error.h` */ // repr is kinda complex and I may have got it wrong. // I tried to follow the stuff at https://rust-lang.github.io/unsafe-code-guidelines/layout/enums.html @@ -22,11 +17,11 @@ pub enum Ipl3ChecksumError { #[error("Failed to convert a FFI string")] StringConversion, - #[error("Unaligned read at offset 0x{offset:X}")] + #[error("Unaligned read at offset 0x{offset:X}. \n (This is probably a library bug, please report me)")] UnalignedRead { offset: usize }, - #[error("Failed to convert bytes at offset 0x{offset:X}")] + #[error("Failed to convert bytes at offset 0x{offset:X} \n (This is probably a library bug, please report me)")] ByteConversion { offset: usize }, - #[error("Tried to access data out of bounds at offset 0x{offset:X}. Requested bytes: 0x{requested_bytes:X}. Buffer length: 0x{buffer_len:X}")] + #[error("Tried to access data out of bounds at offset 0x{offset:X}. Requested bytes: 0x{requested_bytes:X}. Buffer length: 0x{buffer_len:X} \n (This is probably a library bug, please report me)")] OutOfBounds { offset: usize, requested_bytes: usize, @@ -47,8 +42,47 @@ pub enum Ipl3ChecksumError { } #[cfg(feature = "python_bindings")] -impl std::convert::From for PyErr { - fn from(err: Ipl3ChecksumError) -> PyErr { - PyRuntimeError::new_err(err.to_string()) +pub(crate) mod python_bindings { + use pyo3::exceptions::PyRuntimeError; + use pyo3::prelude::*; + + pyo3::create_exception!(ipl3checksum, Ipl3ChecksumError, PyRuntimeError); + + pyo3::create_exception!(ipl3checksum, UnalignedRead, Ipl3ChecksumError); + pyo3::create_exception!(ipl3checksum, ByteConversion, Ipl3ChecksumError); + pyo3::create_exception!(ipl3checksum, OutOfBounds, Ipl3ChecksumError); + pyo3::create_exception!(ipl3checksum, BufferNotBigEnough, Ipl3ChecksumError); + pyo3::create_exception!(ipl3checksum, BufferSizeIsWrong, Ipl3ChecksumError); + pyo3::create_exception!(ipl3checksum, UnableToDetectCIC, Ipl3ChecksumError); + + impl std::convert::From for PyErr { + fn from(err: super::Ipl3ChecksumError) -> PyErr { + match err { + super::Ipl3ChecksumError::UnalignedRead { .. } => { + UnalignedRead::new_err(err.to_string()) + } + super::Ipl3ChecksumError::ByteConversion { .. } => { + ByteConversion::new_err(err.to_string()) + } + super::Ipl3ChecksumError::OutOfBounds { .. } => { + OutOfBounds::new_err(err.to_string()) + } + super::Ipl3ChecksumError::BufferNotBigEnough { .. } => { + BufferNotBigEnough::new_err(err.to_string()) + } + super::Ipl3ChecksumError::BufferSizeIsWrong { .. } => { + BufferSizeIsWrong::new_err(err.to_string()) + } + super::Ipl3ChecksumError::UnableToDetectCIC => { + UnableToDetectCIC::new_err(err.to_string()) + } + #[cfg(feature = "c_bindings")] + super::Ipl3ChecksumError::Okay + | super::Ipl3ChecksumError::NullPointer + | super::Ipl3ChecksumError::StringConversion => { + Ipl3ChecksumError::new_err(err.to_string()) + } + } + } } } diff --git a/src/rs/lib.rs b/src/rs/lib.rs index eda37fd..964ec80 100644 --- a/src/rs/lib.rs +++ b/src/rs/lib.rs @@ -1,4 +1,4 @@ -/* SPDX-FileCopyrightText: © 2023 Decompollaborate */ +/* SPDX-FileCopyrightText: © 2023-2024 Decompollaborate */ /* SPDX-License-Identifier: MIT */ mod checksum; @@ -14,23 +14,75 @@ pub use detect::*; pub use error::*; #[cfg(feature = "python_bindings")] -use pyo3::prelude::*; +mod python_bindings { + use pyo3::prelude::*; -#[cfg(feature = "python_bindings")] -#[pymodule] -fn ipl3checksum(_py: Python<'_>, m: &PyModule) -> PyResult<()> { - m.add_class::()?; - m.add_function(wrap_pyfunction!( - checksum::python_bindings::calculateChecksum, - m - )?)?; - m.add_function(wrap_pyfunction!( - checksum::python_bindings::calculateChecksumAutodetect, - m - )?)?; - m.add_function(wrap_pyfunction!(detect::python_bindings::detectCICRaw, m)?)?; - m.add_function(wrap_pyfunction!(detect::python_bindings::detectCIC, m)?)?; - Ok(()) + #[pymodule] + fn ipl3checksum(py: Python<'_>, m: &PyModule) -> PyResult<()> { + // Classes + m.add_class::()?; + + // Free functions + m.add_function(wrap_pyfunction!( + super::checksum::python_bindings::calculateChecksum, + m + )?)?; + m.add_function(wrap_pyfunction!( + super::checksum::python_bindings::calculateChecksumAutodetect, + m + )?)?; + m.add_function(wrap_pyfunction!( + super::detect::python_bindings::detectCICRaw, + m + )?)?; + m.add_function(wrap_pyfunction!( + super::detect::python_bindings::detectCIC, + m + )?)?; + + // Exceptions + + register_exceptions_module(py, m)?; + + Ok(()) + } + + fn register_exceptions_module(py: Python<'_>, parent_module: &PyModule) -> PyResult<()> { + let child_module = PyModule::new(py, "exceptions")?; + + child_module.add( + "Ipl3ChecksumError", + py.get_type::(), + )?; + + child_module.add( + "UnalignedRead", + py.get_type::(), + )?; + child_module.add( + "ByteConversion", + py.get_type::(), + )?; + child_module.add( + "OutOfBounds", + py.get_type::(), + )?; + child_module.add( + "BufferNotBigEnough", + py.get_type::(), + )?; + child_module.add( + "BufferSizeIsWrong", + py.get_type::(), + )?; + child_module.add( + "UnableToDetectCIC", + py.get_type::(), + )?; + + parent_module.add_submodule(child_module)?; + Ok(()) + } } #[cfg(test)] diff --git a/src/rs/utils.rs b/src/rs/utils.rs index 707a3a0..a243e36 100644 --- a/src/rs/utils.rs +++ b/src/rs/utils.rs @@ -1,4 +1,4 @@ -/* SPDX-FileCopyrightText: © 2023 Decompollaborate */ +/* SPDX-FileCopyrightText: © 2023-2024 Decompollaborate */ /* SPDX-License-Identifier: MIT */ use crate::error::Ipl3ChecksumError; diff --git a/src/rs/version.rs b/src/rs/version.rs index f75b2c7..eccf704 100644 --- a/src/rs/version.rs +++ b/src/rs/version.rs @@ -1,15 +1,15 @@ -/* SPDX-FileCopyrightText: © 2023 Decompollaborate */ +/* SPDX-FileCopyrightText: © 2023-2024 Decompollaborate */ /* SPDX-License-Identifier: MIT */ // Version should be synced with pyproject.toml, Cargo.toml and src/ipl3checksum/__init__.py pub static VERSION_MAJOR: i32 = 1; -pub static VERSION_MINOR: i32 = 1; -pub static VERSION_PATCH: i32 = 1; +pub static VERSION_MINOR: i32 = 2; +pub static VERSION_PATCH: i32 = 0; pub static VERSION_INFO: (i32, i32, i32) = (VERSION_MAJOR, VERSION_MINOR, VERSION_PATCH); // TODO: figure out a way to construct this string by using VERSION_MAJOR, VERSION_MINOR and VERSION_PATCH (concat! and stringify! didn't work) -pub static VERSION_STR: &str = "1.1.1"; +pub static VERSION_STR: &str = "1.2.0"; pub static AUTHOR: &str = "Decompollaborate"; @@ -24,7 +24,7 @@ mod c_bindings { // TODO: construct this from super::VERSION_STR #[no_mangle] - static ipl3checksum_version_str: &[u8] = b"1.1.1\0"; + static ipl3checksum_version_str: &[u8] = b"1.2.0\0"; // TODO: construct this from super::AUTHOR #[no_mangle] diff --git a/tests/calculate_checksum.py b/tests/calculate_checksum.py index 530a1e8..96c17bc 100755 --- a/tests/calculate_checksum.py +++ b/tests/calculate_checksum.py @@ -1,6 +1,6 @@ #!/usr/bin/env python3 -# SPDX-FileCopyrightText: © 2023 Decompollaborate +# SPDX-FileCopyrightText: © 2023-2024 Decompollaborate # SPDX-License-Identifier: MIT from __future__ import annotations diff --git a/tests/check_correct_sum.py b/tests/check_correct_sum.py index 183bdb7..a9dd5d7 100755 --- a/tests/check_correct_sum.py +++ b/tests/check_correct_sum.py @@ -1,6 +1,6 @@ #!/usr/bin/env python3 -# SPDX-FileCopyrightText: © 2023 Decompollaborate +# SPDX-FileCopyrightText: © 2023-2024 Decompollaborate # SPDX-License-Identifier: MIT from __future__ import annotations diff --git a/tests/check_recursive.py b/tests/check_recursive.py index 4a43925..376ec9b 100755 --- a/tests/check_recursive.py +++ b/tests/check_recursive.py @@ -1,6 +1,6 @@ #!/usr/bin/env python3 -# SPDX-FileCopyrightText: © 2023 Decompollaborate +# SPDX-FileCopyrightText: © 2023-2024 Decompollaborate # SPDX-License-Identifier: MIT from __future__ import annotations diff --git a/tests/exception_test.py b/tests/exception_test.py new file mode 100755 index 0000000..74dbbe2 --- /dev/null +++ b/tests/exception_test.py @@ -0,0 +1,22 @@ +#!/usr/bin/env python3 + +# SPDX-FileCopyrightText: © 2024 Decompollaborate +# SPDX-License-Identifier: MIT + +from __future__ import annotations + +import ipl3checksum + +# We want this buffer to be small so it triggers an exception +b = bytes([0,0,0,0]) + +try: + checksum = ipl3checksum.CICKind.CIC_6102_7101.calculateChecksum(b) + + print(f"This code shouldn't run") + print(f"{checksum[0]:08X} {checksum[1]:08X}") + raise RuntimeError() + +except ipl3checksum.exceptions.BufferNotBigEnough as e: + print("We triggered and succesfully catched an exception!") + print(f" e: {e}") diff --git a/tests/gen_dummy_bin.py b/tests/gen_dummy_bin.py index eb05572..7c6da4d 100755 --- a/tests/gen_dummy_bin.py +++ b/tests/gen_dummy_bin.py @@ -1,6 +1,6 @@ #!/usr/bin/env python3 -# SPDX-FileCopyrightText: © 2023 Decompollaborate +# SPDX-FileCopyrightText: © 2023-2024 Decompollaborate # SPDX-License-Identifier: MIT from __future__ import annotations