diff --git a/.github/workflows/mypy.yml b/.github/workflows/mypy.yml index 029b278..d44b301 100644 --- a/.github/workflows/mypy.yml +++ b/.github/workflows/mypy.yml @@ -18,7 +18,7 @@ jobs: - name: Install Dependencies run: | - pip3 install mypy -U + pip3 install -U mypy - name: mypy run: mypy --show-column-numbers --hide-error-context . diff --git a/.github/workflows/publish_crate.yml b/.github/workflows/publish_crate.yml new file mode 100644 index 0000000..cfdb811 --- /dev/null +++ b/.github/workflows/publish_crate.yml @@ -0,0 +1,42 @@ +name: Build and upload Rust crate + +# Build on every branch push, tag push, and pull request change: +on: [push, pull_request] + +jobs: + build_rust: + name: Build Rust stuff and run Rust tests + runs-on: ubuntu-latest + + steps: + - name: Checkout reposistory + uses: actions/checkout@v4 + + - name: Setup Rust toolchain + uses: actions-rs/toolchain@v1 + with: + toolchain: stable + override: true + + - name: Check format + run: cargo fmt --check + + - name: Setup clippy + run: rustup component add clippy + + - name: Run clippy + run: cargo clippy --all-targets --all-features -- -D warnings + + - name: Build Rust package + run: cargo build --release --workspace + + - name: Build Rust tests + run: cargo test --workspace + + - name: Publish dry run + if: github.event_name == 'push' && !startsWith(github.ref, 'refs/tags/') + run: cargo publish --dry-run + + - name: Upload crate + if: github.event_name == 'push' && startsWith(github.ref, 'refs/tags/') + run: cargo publish --token ${{ secrets.CARGO_REGISTRY_TOKEN }} diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 36937c5..7c9fc6a 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -12,6 +12,10 @@ jobs: - name: Checkout repository uses: actions/checkout@v4 + - name: Install Dependencies + run: | + pip3 install -U maturin + - name: Install local ipl3checksum run: pip install . diff --git a/.github/workflows/tests_other_repo.yml b/.github/workflows/tests_other_repo.yml index 4aaeaff..3bf6d4f 100644 --- a/.github/workflows/tests_other_repo.yml +++ b/.github/workflows/tests_other_repo.yml @@ -4,8 +4,8 @@ name: Test other repo on: [push, pull_request] jobs: - build_repo: - name: Test other repo + test_other_repo_py: + name: Test other repo (Python) runs-on: ubuntu-latest strategy: @@ -15,6 +15,10 @@ jobs: - name: Checkout repository uses: actions/checkout@v4 + - name: Install Dependencies + run: | + python3 -m pip install -U maturin + - name: Install local ipl3checksum run: python3 -m pip install . diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..861e9e5 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,6 @@ +{ + "rust-analyzer.cargo.features": [ + "c_bindings", + "python_bindings" + ] +} \ No newline at end of file diff --git a/CHANGELOG.md b/CHANGELOG.md index 7a837fc..1bf82d9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,19 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +### Added + +- Add Rust support +- New static method `CICKind.fromHashMd5`. + - Returns a CIC kind based on the passed md5 hash. + +### Changed + +- Library was reimplemented in Rust, allowing faster runtime calculation. + - The Python API is still the same + +### Fixed + - Fix links in `CHANGELOG.md` ## [1.0.1] - 2023-09-21 diff --git a/Cargo.lock b/Cargo.lock new file mode 100644 index 0000000..cfeddea --- /dev/null +++ b/Cargo.lock @@ -0,0 +1,287 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 3 + +[[package]] +name = "autocfg" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa" + +[[package]] +name = "bitflags" +version = "1.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" + +[[package]] +name = "cfg-if" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" + +[[package]] +name = "heck" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8" + +[[package]] +name = "indoc" +version = "2.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e186cfbae8084e513daff4240b4797e342f988cecda4fb6c939150f96315fd8" + +[[package]] +name = "ipl3checksum" +version = "1.1.0" +dependencies = [ + "md5", + "pyo3", +] + +[[package]] +name = "libc" +version = "0.2.151" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "302d7ab3130588088d277783b1e2d2e10c9e9e4a16dd9050e6ec93fb3e7048f4" + +[[package]] +name = "lock_api" +version = "0.4.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c168f8615b12bc01f9c17e2eb0cc07dcae1940121185446edc3744920e8ef45" +dependencies = [ + "autocfg", + "scopeguard", +] + +[[package]] +name = "md5" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "490cc448043f947bae3cbee9c203358d62dbee0db12107a74be5c30ccfd09771" + +[[package]] +name = "memoffset" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a634b1c61a95585bd15607c6ab0c4e5b226e695ff2800ba0cdccddf208c406c" +dependencies = [ + "autocfg", +] + +[[package]] +name = "once_cell" +version = "1.19.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3fdb12b2476b595f9358c5161aa467c2438859caa136dec86c26fdd2efe17b92" + +[[package]] +name = "parking_lot" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3742b2c103b9f06bc9fff0a37ff4912935851bee6d36f3c02bcc755bcfec228f" +dependencies = [ + "lock_api", + "parking_lot_core", +] + +[[package]] +name = "parking_lot_core" +version = "0.9.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4c42a9226546d68acdd9c0a280d17ce19bfe27a46bf68784e4066115788d008e" +dependencies = [ + "cfg-if", + "libc", + "redox_syscall", + "smallvec", + "windows-targets", +] + +[[package]] +name = "proc-macro2" +version = "1.0.70" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "39278fbbf5fb4f646ce651690877f89d1c5811a3d4acb27700c1cb3cdb78fd3b" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "pyo3" +version = "0.20.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "04e8453b658fe480c3e70c8ed4e3d3ec33eb74988bd186561b0cc66b85c3bc4b" +dependencies = [ + "cfg-if", + "indoc", + "libc", + "memoffset", + "parking_lot", + "pyo3-build-config", + "pyo3-ffi", + "pyo3-macros", + "unindent", +] + +[[package]] +name = "pyo3-build-config" +version = "0.20.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a96fe70b176a89cff78f2fa7b3c930081e163d5379b4dcdf993e3ae29ca662e5" +dependencies = [ + "once_cell", + "target-lexicon", +] + +[[package]] +name = "pyo3-ffi" +version = "0.20.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "214929900fd25e6604661ed9cf349727c8920d47deff196c4e28165a6ef2a96b" +dependencies = [ + "libc", + "pyo3-build-config", +] + +[[package]] +name = "pyo3-macros" +version = "0.20.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dac53072f717aa1bfa4db832b39de8c875b7c7af4f4a6fe93cdbf9264cf8383b" +dependencies = [ + "proc-macro2", + "pyo3-macros-backend", + "quote", + "syn", +] + +[[package]] +name = "pyo3-macros-backend" +version = "0.20.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7774b5a8282bd4f25f803b1f0d945120be959a36c72e08e7cd031c792fdfd424" +dependencies = [ + "heck", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "quote" +version = "1.0.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5267fca4496028628a95160fc423a33e8b2e6af8a5302579e322e4b520293cae" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "redox_syscall" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4722d768eff46b75989dd134e5c353f0d6296e5aaa3132e776cbdb56be7731aa" +dependencies = [ + "bitflags", +] + +[[package]] +name = "scopeguard" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" + +[[package]] +name = "smallvec" +version = "1.11.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4dccd0940a2dcdf68d092b8cbab7dc0ad8fa938bf95787e1b916b0e3d0e8e970" + +[[package]] +name = "syn" +version = "2.0.41" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "44c8b28c477cc3bf0e7966561e3460130e1255f7a1cf71931075f1c5e7a7e269" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "target-lexicon" +version = "0.12.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "14c39fd04924ca3a864207c66fc2cd7d22d7c016007f9ce846cbb9326331930a" + +[[package]] +name = "unicode-ident" +version = "1.0.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b" + +[[package]] +name = "unindent" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c7de7d73e1754487cb58364ee906a499937a0dfabd86bcb980fa99ec8c8fa2ce" + +[[package]] +name = "windows-targets" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a2fa6e2155d7247be68c096456083145c183cbbbc2764150dda45a87197940c" +dependencies = [ + "windows_aarch64_gnullvm", + "windows_aarch64_msvc", + "windows_i686_gnu", + "windows_i686_msvc", + "windows_x86_64_gnu", + "windows_x86_64_gnullvm", + "windows_x86_64_msvc", +] + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc" + +[[package]] +name = "windows_i686_gnu" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e" + +[[package]] +name = "windows_i686_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538" diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..d9d8dad --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,28 @@ +# SPDX-FileCopyrightText: © 2023 Decompollaborate +# SPDX-License-Identifier: MIT + +[package] +name = "ipl3checksum" +# Version should be synced with src/ipl3checksum/__init__.py and pyproject.toml +version = "1.1.0" +edition = "2021" +description = "Library to calculate the IPL3 checksum for N64 ROMs" +repository = "https://github.com/decompollaborate/ipl3checksum" +license = "MIT" + +[lib] +name = "ipl3checksum" +path = "src/rs/lib.rs" +crate-type = ["lib", "cdylib"] + +[dependencies] +md5 = "0.7.0" +pyo3 = { version="0.20.0", features = ["extension-module"], optional = true } +# thiserror = "1.0" + +# [dev-dependencies] +# rstest = "0.18.2" + +[features] +c_bindings = [] +python_bindings = ["dep:pyo3"] diff --git a/README.md b/README.md index 127d633..d668f55 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,10 @@ -# ipl3checksum ![PyPI - Downloads] ![GitHub License] ![GitHub release (latest SemVer)] ![PyPI] ![GitHub contributors] +# ipl3checksum + +![PyPI - Downloads] +![GitHub License] +![GitHub release (latest SemVer)] +![PyPI] +![GitHub contributors] [PyPI - Downloads]: [GitHub License]: @@ -6,7 +12,7 @@ [PyPI]: [GitHub contributors]: -A Python library to calculate the IPL3 checksum for N64 ROMs. +A Python and Rust library to calculate the IPL3 checksum for N64 ROMs. ## How to use it? @@ -17,7 +23,10 @@ romBytes = # A big endian bytes-like object cickind = ipl3checksum.CICKind.CIC_6102_7101 checksum = ipl3checksum.calculateChecksum(romBytes, cickind) -assert checksum is not None # Not able to compute the checksum, probably because rom was too small + +# If this assert fails it is because the library was not able to compute the +# checksum, probably because the passed rom was too small +assert checksum is not None print(f"{checksum[0]:08X}") print(f"{checksum[1]:08X}") @@ -27,7 +36,8 @@ This library also contains a CIC detector: ```py cickind = ipl3checksum.detectCIC(romBytes) -print(cickind) # Either a `ipl3checksum.CICKind` or None if was not able to detect the CIC +# Either a `ipl3checksum.CICKind` or None if was not able to detect the CIC +print(cickind) ``` ## Features @@ -36,6 +46,7 @@ print(cickind) # Either a `ipl3checksum.CICKind` or None if was not able to dete - Can calculate the checksum of a ROM using the algorithm of any of the supported CIC variants. - Can detect any of the supported CIC variants. +- Fast calculation written in Rust. ### Restrictions/requirements @@ -44,10 +55,13 @@ supported CIC variants. - Since the checksum algorithm is calculated on the first MiB after IPL3 (from `0x1000` to `0x101000`), then the library expects the passed ROM to be at least `0x101000` bytes long, otherwise the library will reject the ROM. - - If it is not the case, then pad your ROM with zeroes to that size. + - If your ROM is not big enough then it is suggested then pad your ROM with + zeroes until it reaches that size. ## Installing +### Python version + First you need to install the library, one way of doing it is via `pip`. ```bash @@ -58,21 +72,25 @@ If you use a `requirements.txt` file in your repository, then you can add this library with the following line: ```txt -ipl3checksum>=1.0.0,<2.0.0 +ipl3checksum>=1.1.0,<2.0.0 `````` Now you can invoke the library from your script. -### Development version +#### Development version -The unstable development version is located at the [develop](https://github.com/Decompollaborate/ipl3checksum/tree/develop) +The unstable development version is located at the +[develop](https://github.com/Decompollaborate/ipl3checksum/tree/develop) branch. PRs should be made into that branch instead of the main one. -The recommended way to install a locally cloned repo is by passing the `-e` -(editable) flag to `pip`. +Since this library uses Rust code then you'll need a Rust compiler installed +on your system. To build the Python bindings you'll also need `maturin` +installed via `pip`. + +The recommended way to install a locally cloned repo the following. ```bash -python3 -m pip install -e . +python3 -m pip install . ``` In case you want to mess with the latest development version without wanting to @@ -86,6 +104,22 @@ python3 -m pip install git+https://github.com/Decompollaborate/ipl3checksum.git@ NOTE: Installing the development version is not recommended unless you know what you are doing. Proceed at your own risk. +### Rust version + +See this crate at . + +To add this library to your project using Cargo: + +```bash +cargo add ipl3checksum +``` + +Or add the following line manually to your `Cargo.toml` file: + +```toml +ipl3checksum = "1.1.0" +``` + ## Versioning and changelog This library follows [Semantic Versioning](https://semver.org/spec/v2.0.0.html). diff --git a/pyproject.toml b/pyproject.toml index 85329df..b8f5f34 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -3,19 +3,24 @@ [project] name = "ipl3checksum" -version = "1.0.1" +# Version should be synced with src/ipl3checksum/__init__.py and Cargo.toml +version = "1.1.0.dev0" description = "Library to calculate the IPL3 checksum for N64 ROMs" readme = "README.md" requires-python = ">=3.7" -dynamic = ["dependencies"] +classifiers = [ + "Programming Language :: Rust", + "Programming Language :: Python :: Implementation :: CPython", + "Programming Language :: Python :: Implementation :: PyPy", +] [project.urls] "Homepage" = "https://github.com/decompollaborate/ipl3checksum" "Bug Tracker" = "https://github.com/decompollaborate/ipl3checksum/issues" [build-system] -requires = ["hatchling", "hatch-requirements-txt"] -build-backend = "hatchling.build" +requires = ["maturin>=1.2,<2.0"] +build-backend = "maturin" [project.scripts] ipl3checksum = "ipl3checksum.frontends.climain:ipl3checksumMain" @@ -25,3 +30,8 @@ skip = ["cp36-*"] [tool.setuptools.package-data] ipl3checksum = ["py.typed"] + +[tool.maturin] +features = ["pyo3/extension-module", "python_bindings"] +# https://github.com/PyO3/maturin/blob/0dee40510083c03607834c821eea76964140a126/Readme.md#mixed-rustpython-projects +python-source = "src" diff --git a/src/ipl3checksum/__init__.py b/src/ipl3checksum/__init__.py index c8086d8..b4597ad 100644 --- a/src/ipl3checksum/__init__.py +++ b/src/ipl3checksum/__init__.py @@ -5,16 +5,9 @@ from __future__ import annotations -__version_info__: tuple[int, int, int] = (1, 0, 1) -__version__ = ".".join(map(str, __version_info__)) +# Version should be synced with pyproject.toml and Cargo.toml +__version_info__: tuple[int, int, int] = (1, 1, 0) +__version__ = ".".join(map(str, __version_info__)) + ".dev0" __author__ = "Decompollaborate" -from . import utils as utils - -from .cickinds import CICKind as CICKind - -from .checksum import calculateChecksum as calculateChecksum -from .checksum import calculateChecksumAutodetect as calculateChecksumAutodetect - -from .detect import detectCIC as detectCIC -from .detect import detectCICRaw as detectCICRaw +from .ipl3checksum import * diff --git a/src/ipl3checksum/checksum.py b/src/ipl3checksum/checksum.py deleted file mode 100644 index f12d996..0000000 --- a/src/ipl3checksum/checksum.py +++ /dev/null @@ -1,183 +0,0 @@ -#!/usr/bin/env python3 - -# SPDX-FileCopyrightText: © 2023 Decompollaborate -# SPDX-License-Identifier: MIT - -from __future__ import annotations - -import struct - -from . import utils -from .cickinds import CICKind -from .detect import detectCIC - - -def readWordFromRam(romWords: list[int], entrypointRam: int, ramAddr: int) -> int: - return romWords[utils.u32(ramAddr - entrypointRam + 0x1000) // 4] - - -def calculateChecksum(romBytes: bytes, kind: CICKind) -> tuple[int, int]|None: - """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. - kind (CICKind): The CIC kind variation used to calculate the checksum. - - Returns: - tuple[int, int]|None: If no error happens then the calculated checksum is returned, stored as a tuple - containing two 32-bits words. Otherwise, `None` is returned. Possible errors: - - `romBytes` not being big enough - """ - - if len(romBytes) < 0x101000: - return None - - romWords = list(struct.unpack_from(f">{0x101000//4}I", romBytes)) - - seed = kind.getSeed() - magic = kind.getMagic() - - s6 = seed - - a0 = romWords[8//4] - if kind == CICKind.CIC_X103: - a0 -= 0x100000 - if kind == CICKind.CIC_X106: - a0 -= 0x200000 - entrypointRam = a0 - - at = magic - lo = s6 * at - - if kind == CICKind.CIC_X105: - s6 = 0xA0000200 - - ra = 0x100000 - - v1 = 0 - t0 = 0 - - t1 = a0 - - t5 = 0x20 - - v0 = utils.u32(lo) - v0 += 1 - - a3 = v0 - t2 = v0 - t3 = v0 - s0 = v0 - a2 = v0 - t4 = v0 - - # poor man's do while - LA40005F0_loop = True - while LA40005F0_loop: - # v0 = *t1 - v0 = readWordFromRam(romWords, entrypointRam, t1) - - v1 = utils.u32(a3 + v0) - - at = utils.u32(v1) < utils.u32(a3) - - a1 = v1 - # if (at == 0) goto LA4000608; - - if at != 0: - t2 = utils.u32(t2 + 0x1) - - # LA4000608 - v1 = v0 & 0x1F - t7 = utils.u32(t5 - v1) - - - t8 = utils.u32(v0 >> t7) - t6 = utils.u32(v0 << v1) - - a0 = t6 | t8 - at = utils.u32(a2) < utils.u32(v0) - a3 = a1 - - t3 = t3 ^ v0 - - s0 = utils.u32(s0 + a0) - # if (at == 0) goto LA400063C; - if (at != 0): - t9 = a3 ^ v0 - - a2 = t9 ^ a2 - # goto LA4000640; - - # LA400063C: - else: - a2 = 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 - t7 = romWords[(s6 - 0xA0000004 + 0x000554) // 4] - - t0 = utils.u32(t0 + 0x4) - s6 = utils.u32(s6 + 0x4) - t7 = v0 ^ t7 - - t4 = utils.u32(t7 + t4) - - t7 = 0xA00002FF - - t1 = utils.u32(t1 + 0x4) - - s6 = utils.u32(s6 & t7) - else: - t0 = utils.u32(t0 + 0x4) - t7 = v0 ^ s0 - t1 = utils.u32(t1 + 0x4) - - t4 = utils.u32(t7 + t4) - - # if (t0 != ra) goto LA40005F0; - if t0 == ra: - LA40005F0_loop = False - - - if kind == CICKind.CIC_X103: - t6 = a3 ^ t2 - a3 = utils.u32(t6 + t3) - t8 = s0 ^ a2 - s0 = utils.u32(t8 + t4) - elif kind == CICKind.CIC_X106: - t6 = utils.u32(a3 * t2) - a3 = utils.u32(t6 + t3) - t8 = utils.u32(s0 * a2) - s0 = utils.u32(t8 + t4) - else: - t6 = a3 ^ t2 - a3 = t6 ^ t3 - t8 = s0 ^ a2 - s0 = t8 ^ t4 - - return (a3, s0) - -def calculateChecksumAutodetect(romBytes: bytes) -> tuple[int, int]|None: - """Calculates the checksum required by an official CIC of a N64 ROM. - - This function will try to autodetect the CIC kind automatically. If it fails to detect it then it will return `None`. - - 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]|None: If no error happens then the calculated checksum is returned, stored as a tuple - containing two 32-bits words. Otherwise, `None` is returned. Possible errors: - - `romBytes` not being big enough - - Not able to detect the CIC kind - """ - - kind = detectCIC(romBytes) - - if kind is None: - return None - - return calculateChecksum(romBytes, kind) diff --git a/src/ipl3checksum/checksum.pyi b/src/ipl3checksum/checksum.pyi new file mode 100644 index 0000000..c709541 --- /dev/null +++ b/src/ipl3checksum/checksum.pyi @@ -0,0 +1,36 @@ +#!/usr/bin/env python3 + +# SPDX-FileCopyrightText: © 2023 Decompollaborate +# SPDX-License-Identifier: MIT + +from __future__ import annotations + +from .cickinds import CICKind + +def calculateChecksum(romBytes: bytes, kind: CICKind) -> tuple[int, int]|None: + """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. + kind (CICKind): The CIC kind variation used to calculate the checksum. + + Returns: + tuple[int, int]|None: If no error happens then the calculated checksum is returned, stored as a tuple + containing two 32-bits words. Otherwise, `None` is returned. Possible errors: + - `romBytes` not being big enough + """ + +def calculateChecksumAutodetect(romBytes: bytes) -> tuple[int, int]|None: + """Calculates the checksum required by an official CIC of a N64 ROM. + + This function will try to autodetect the CIC kind automatically. If it fails to detect it then it will return `None`. + + 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]|None: If no error happens then the calculated checksum is returned, stored as a tuple + containing two 32-bits words. Otherwise, `None` is returned. Possible errors: + - `romBytes` not being big enough + - Not able to detect the CIC kind + """ diff --git a/src/ipl3checksum/cickinds.py b/src/ipl3checksum/cickinds.py deleted file mode 100644 index 3beb0f3..0000000 --- a/src/ipl3checksum/cickinds.py +++ /dev/null @@ -1,82 +0,0 @@ -#!/usr/bin/env python3 - -# SPDX-FileCopyrightText: © 2023 Decompollaborate -# SPDX-License-Identifier: MIT - -from __future__ import annotations - -import enum - -class CICKind(enum.Enum): - CIC_6101 = enum.auto() - CIC_6102_7101 = enum.auto() - CIC_7102 = enum.auto() - CIC_X103 = enum.auto() # Both 6103 and 7103 - # 6104/7104 does not exist - CIC_X105 = enum.auto() # Both 6105 and 7105 - CIC_X106 = enum.auto() # Both 6106 and 7106 - - - 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 - """ - return CICSeeds[self] - - def getMagic(self) -> int: - """ - Magic value hardcoded inside the IPL3 itself - """ - return CICMagics[self] - - def getHashMd5(self) -> str: - """ - Expected md5 hash of the IPL3 blob - """ - return CICHashMd5[self] - - @staticmethod - def fromValue(value: int) -> CICKind|None: - if value == 6102 or value == 7101: - return CICKind.CIC_6102_7101 - if value == 6101: - return CICKind.CIC_6101 - if value == 7102: - return CICKind.CIC_7102 - if value == 6103 or value == 7103: - return CICKind.CIC_X103 - if value == 6105 or value == 7105: - return CICKind.CIC_X105 - if value == 6106 or value == 7106: - return CICKind.CIC_X106 - - return None - - -CICSeeds: dict[CICKind, int] = { - CICKind.CIC_6101: 0x3F, - CICKind.CIC_6102_7101: 0x3F, - CICKind.CIC_7102: 0x3F, - CICKind.CIC_X103: 0x78, - CICKind.CIC_X105: 0x91, - CICKind.CIC_X106: 0x85, -} - -CICMagics: dict[CICKind, int] = { - CICKind.CIC_6101: 0x5D588B65, - CICKind.CIC_6102_7101: 0x5D588B65, - CICKind.CIC_7102: 0x5D588B65, - CICKind.CIC_X103: 0x6C078965, - CICKind.CIC_X105: 0x5D588B65, - CICKind.CIC_X106: 0x6C078965, -} - -CICHashMd5: dict[CICKind, str] = { - CICKind.CIC_6101: "900b4a5b68edb71f4c7ed52acd814fc5", - CICKind.CIC_6102_7101: "e24dd796b2fa16511521139d28c8356b", - CICKind.CIC_7102: "955894c2e40a698bf98a67b78a4e28fa", - CICKind.CIC_X103: "319038097346e12c26c3c21b56f86f23", - CICKind.CIC_X105: "ff22a296e55d34ab0a077dc2ba5f5796", - CICKind.CIC_X106: "6460387749ac0bd925aa5430bc7864fe", -} diff --git a/src/ipl3checksum/cickinds.pyi b/src/ipl3checksum/cickinds.pyi new file mode 100644 index 0000000..256840e --- /dev/null +++ b/src/ipl3checksum/cickinds.pyi @@ -0,0 +1,44 @@ +#!/usr/bin/env python3 + +# SPDX-FileCopyrightText: © 2023 Decompollaborate +# SPDX-License-Identifier: MIT + +from __future__ import annotations + +class CICKind(): + CIC_6101: CICKind + CIC_6102_7101: CICKind + CIC_7102: CICKind + CIC_X103: CICKind # Both 6103 and 7103 + # 6104/7104 does not exist + CIC_X105: CICKind # Both 6105 and 7105 + CIC_X106: CICKind # Both 6106 and 7106 + + + 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 + """ + + def getMagic(self) -> int: + """ + Magic value hardcoded inside the IPL3 itself + """ + + def getHashMd5(self) -> str: + """ + Expected md5 hash of the IPL3 blob + """ + + @staticmethod + def fromHashMd5(hash_str: str) -> CICKind|None: + ... + + @staticmethod + def fromValue(value: int) -> CICKind|None: + ... + + @property + def name(self) -> str: + ... diff --git a/src/ipl3checksum/detect.py b/src/ipl3checksum/detect.pyi similarity index 71% rename from src/ipl3checksum/detect.py rename to src/ipl3checksum/detect.pyi index cea20b5..d29892f 100644 --- a/src/ipl3checksum/detect.py +++ b/src/ipl3checksum/detect.pyi @@ -5,9 +5,7 @@ from __future__ import annotations -from . import utils -from .cickinds import CICKind, CICHashMd5 - +from .cickinds import CICKind def detectCICRaw(rawBytes: bytes) -> CICKind|None: """Tries to detect an IPL3 binary. @@ -21,17 +19,6 @@ def detectCICRaw(rawBytes: bytes) -> CICKind|None: CICKind|None: The detected CIC kind, or `None` if was not able to detect the CIC kind. """ - if len(rawBytes) < 0xFC0: - return None - - bytesHash = utils.getHashMd5(rawBytes[:0xFC0]) - - for kind, expectedHash in CICHashMd5.items(): - if bytesHash == expectedHash: - return kind - - return None - def detectCIC(romBytes: bytes) -> CICKind|None: """Tries to detect an IPL3 in a ROM. @@ -44,5 +31,3 @@ def detectCIC(romBytes: bytes) -> CICKind|None: Returns: CICKind|None: The detected CIC kind, or `None` if was not able to detect the CIC kind. """ - - return detectCICRaw(romBytes[0x40:0x1000]) diff --git a/src/ipl3checksum/ipl3checksum.pyi b/src/ipl3checksum/ipl3checksum.pyi new file mode 100644 index 0000000..9dda0b8 --- /dev/null +++ b/src/ipl3checksum/ipl3checksum.pyi @@ -0,0 +1,14 @@ +#!/usr/bin/env python3 + +# SPDX-FileCopyrightText: © 2023 Decompollaborate +# SPDX-License-Identifier: MIT + +from __future__ import annotations + +from .cickinds import CICKind as CICKind + +from .checksum import calculateChecksum as calculateChecksum +from .checksum import calculateChecksumAutodetect as calculateChecksumAutodetect + +from .detect import detectCIC as detectCIC +from .detect import detectCICRaw as detectCICRaw diff --git a/src/ipl3checksum/utils.py b/src/ipl3checksum/utils.py deleted file mode 100644 index ee0ce30..0000000 --- a/src/ipl3checksum/utils.py +++ /dev/null @@ -1,14 +0,0 @@ -#!/usr/bin/env python3 - -# SPDX-FileCopyrightText: © 2023 Decompollaborate -# SPDX-License-Identifier: MIT - -from __future__ import annotations - -import hashlib - -def u32(value: int) -> int: - return value & 0xFFFFFFFF - -def getHashMd5(bytes: bytes) -> str: - return str(hashlib.md5(bytes).hexdigest()) diff --git a/src/rs/checksum.rs b/src/rs/checksum.rs new file mode 100644 index 0000000..8f68cd5 --- /dev/null +++ b/src/rs/checksum.rs @@ -0,0 +1,313 @@ +/* SPDX-FileCopyrightText: © 2023 Decompollaborate */ +/* SPDX-License-Identifier: MIT */ + +use crate::cickinds::CICKind; +use crate::{detect, 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] +} + +/// 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. +/// * `kind` - The CIC kind variation used to calculate the checksum. +/// +/// ## Return +/// +/// * If no error happens then the calculated checksum is returned, stored as a tuple +/// containing two 32-bits words. Otherwise, `None` is returned. +/// Possible errors: +/// - `rom_bytes` not being big enough +/// +/// ## Examples +/// +/// ``` +/// use ipl3checksum; +/// let bytes = vec![0; 0x101000]; +/// let kind = ipl3checksum::CICKind::CIC_6102_7101; +/// let checksum = ipl3checksum::calculate_checksum(&bytes, &kind).unwrap(); +/// println!("{:08X} {:08X}", checksum.0, checksum.1); +/// ``` +pub fn calculate_checksum(rom_bytes: &[u8], kind: &CICKind) -> Option<(u32, u32)> { + if rom_bytes.len() < 0x101000 { + return None; + } + + let rom_words = utils::read_u32_vec(rom_bytes, 0, 0x101000 / 4); + + let seed = kind.get_seed(); + let magic = kind.get_magic(); + + let mut s6 = seed; + + let mut a0 = rom_words[8 / 4]; + if *kind == CICKind::CIC_X103 { + 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 ra = 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 mut a3 = v0; + let mut t2 = v0; + let mut t3 = v0; + let mut s0 = v0; + let mut a2 = v0; + let mut t4 = v0; + + // 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); + + //v1 = utils.u32(a3 + v0); + let mut v1 = a3.wrapping_add(v0); + + //at = utils.u32(v1) < utils.u32(a3); + at = if v1 < a3 { 1 } else { 0 }; + + let a1 = v1; + // if (at == 0) goto LA4000608; + + if at != 0 { + //t2 = utils.u32(t2 + 0x1) + 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; + + a2 ^= t9; + // goto LA4000640; + + // LA400063C: + } 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); + + // s6 = utils.u32(s6 & t7); + s6 &= t7; + } else { + // t0 = utils.u32(t0 + 0x4); + t0 = t0.wrapping_add(0x4); + + let t7 = v0 ^ s0; + + // t1 = utils.u32(t1 + 0x4); + t1 = t1.wrapping_add(0x4); + + // t4 = utils.u32(t7 + t4); + t4 = t7.wrapping_add(t4); + } + + // if (t0 != ra) goto LA40005F0; + if t0 == ra { + loop_variable = false; + } + } + + if *kind == CICKind::CIC_X103 { + 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; + } + + Some((a3, s0)) +} + +/// Calculates the checksum required by an official CIC of a N64 ROM. +/// +/// This function will try to autodetect the CIC kind automatically. If it fails to detect it then it will return `None`. +/// +/// ## 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. Otherwise, `None` is returned. +/// Possible errors: +/// - `rom_bytes` not being big enough +/// - Not able to detect the CIC kind +/// +/// ## Examples +/// +/// ``` +/// use ipl3checksum; +/// let bytes = vec![0; 0x101000]; +/// let checksum = ipl3checksum::calculate_checksum_autodetect(&bytes); +/// /* This will return `None` because there's no ipl3 binary on an array of zeroes */ +/// assert!(checksum.is_none()); +/// ``` +pub fn calculate_checksum_autodetect(rom_bytes: &[u8]) -> Option<(u32, u32)> { + let kind = detect::detect_cic(rom_bytes)?; + + calculate_checksum(rom_bytes, &kind) +} + +#[cfg(test)] +mod tests { + use crate::{cickinds::CICKind, utils}; + use std::fs; + + #[test] + fn test_dummy_files() -> Result<(), ()> { + println!("asdf"); + + for path_result in fs::read_dir("tests/dummytests").unwrap() { + let ipl3_folder = path_result.unwrap(); + let folder_name = ipl3_folder.file_name(); + + println!("{:?}", folder_name); + + let kind = match folder_name.to_str().unwrap() { + "CIC_6101" => CICKind::CIC_6101, + "CIC_6102_7101" => CICKind::CIC_6102_7101, + "CIC_7102" => CICKind::CIC_7102, + "CIC_X103" => CICKind::CIC_X103, + "CIC_X105" => CICKind::CIC_X105, + "CIC_X106" => CICKind::CIC_X106, + _ => panic!("Unknown cic kind"), + }; + println!("CIC Kind: {:?}", kind); + + for bin_path_result in fs::read_dir(ipl3_folder.path()).unwrap() { + let bin_path = bin_path_result.unwrap(); + + println!("{:?}", bin_path); + + println!(" Reading..."); + + let bin_bytes = fs::read(&bin_path.path()).unwrap(); + + println!(" Calculating checksum..."); + let checksum = super::calculate_checksum(&bin_bytes, &kind).unwrap(); + + println!( + " Calculated checksum is: 0x{:08X} 0x{:08X}", + checksum.0, checksum.1 + ); + + println!(" Checking checksum..."); + let bin_checksum = utils::read_u32_vec(&bin_bytes, 0x10, 2); + + println!( + " Expected checksum is: 0x{:08X} 0x{:08X}", + bin_checksum[0], bin_checksum[1] + ); + + assert_eq!(checksum.0, bin_checksum[0]); + assert_eq!(checksum.1, bin_checksum[1]); + + println!(" {:?} OK", bin_path); + + println!(); + } + + println!(); + } + Ok(()) + } +} + +#[cfg(feature = "python_bindings")] +#[allow(non_snake_case)] +pub(crate) mod python_bindings { + use pyo3::prelude::*; + + #[pyfunction] + pub(crate) fn calculateChecksum(rom_bytes: &[u8], kind: &super::CICKind) -> Option<(u32, u32)> { + super::calculate_checksum(rom_bytes, kind) + } + + #[pyfunction] + pub(crate) fn calculateChecksumAutodetect(rom_bytes: &[u8]) -> Option<(u32, u32)> { + super::calculate_checksum_autodetect(rom_bytes) + } +} diff --git a/src/rs/cickinds.rs b/src/rs/cickinds.rs new file mode 100644 index 0000000..b2f4648 --- /dev/null +++ b/src/rs/cickinds.rs @@ -0,0 +1,121 @@ +/* SPDX-FileCopyrightText: © 2023 Decompollaborate */ +/* SPDX-License-Identifier: MIT */ + +#[cfg(feature = "python_bindings")] +use pyo3::prelude::*; + +#[cfg_attr(feature = "python_bindings", pyclass(module = "ipl3checksum"))] +#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash)] +#[allow(non_camel_case_types)] +/// Enum that represents a CIC kind +pub enum CICKind { + CIC_6101, + CIC_6102_7101, + CIC_7102, + CIC_X103, // Both 6103 and 7103 + // 6104/7104 does not exist + CIC_X105, // Both 6105 and 7105 + CIC_X106, // Both 6106 and 7106 +} + +impl CICKind { + pub fn get_seed(&self) -> u32 { + match self { + CICKind::CIC_6101 => 0x3F, + CICKind::CIC_6102_7101 => 0x3F, + CICKind::CIC_7102 => 0x3F, + CICKind::CIC_X103 => 0x78, + CICKind::CIC_X105 => 0x91, + CICKind::CIC_X106 => 0x85, + } + } + + pub fn get_magic(&self) -> u32 { + match self { + CICKind::CIC_6101 => 0x5D588B65, + CICKind::CIC_6102_7101 => 0x5D588B65, + CICKind::CIC_7102 => 0x5D588B65, + CICKind::CIC_X103 => 0x6C078965, + CICKind::CIC_X105 => 0x5D588B65, + CICKind::CIC_X106 => 0x6C078965, + } + } + + pub fn get_hash_md5(&self) -> &str { + match self { + CICKind::CIC_6101 => "900b4a5b68edb71f4c7ed52acd814fc5", + CICKind::CIC_6102_7101 => "e24dd796b2fa16511521139d28c8356b", + CICKind::CIC_7102 => "955894c2e40a698bf98a67b78a4e28fa", + CICKind::CIC_X103 => "319038097346e12c26c3c21b56f86f23", + CICKind::CIC_X105 => "ff22a296e55d34ab0a077dc2ba5f5796", + CICKind::CIC_X106 => "6460387749ac0bd925aa5430bc7864fe", + } + } + + pub fn from_hash_md5(hash_str: &str) -> Option { + match hash_str { + "900b4a5b68edb71f4c7ed52acd814fc5" => Some(CICKind::CIC_6101), + "e24dd796b2fa16511521139d28c8356b" => Some(CICKind::CIC_6102_7101), + "955894c2e40a698bf98a67b78a4e28fa" => Some(CICKind::CIC_7102), + "319038097346e12c26c3c21b56f86f23" => Some(CICKind::CIC_X103), + "ff22a296e55d34ab0a077dc2ba5f5796" => Some(CICKind::CIC_X105), + "6460387749ac0bd925aa5430bc7864fe" => Some(CICKind::CIC_X106), + _ => None, + } + } + + pub fn from_value(value: usize) -> Option { + match value { + 6101 => Some(CICKind::CIC_6101), + 6102 | 7101 => Some(CICKind::CIC_6102_7101), + 7102 => Some(CICKind::CIC_7102), + 6103 | 7103 => Some(CICKind::CIC_X103), + 6105 | 7105 => Some(CICKind::CIC_X105), + 6106 | 7106 => Some(CICKind::CIC_X106), + _ => None, + } + } +} + +#[cfg(feature = "python_bindings")] +#[allow(non_snake_case)] +mod python_bindings { + use pyo3::prelude::*; + + #[pymethods] + impl super::CICKind { + pub fn getSeed(&self) -> u32 { + self.get_seed() + } + + pub fn getMagic(&self) -> u32 { + self.get_magic() + } + + pub fn getHashMd5(&self) -> &str { + self.get_hash_md5() + } + + #[staticmethod] + pub fn fromHashMd5(hash_str: &str) -> Option { + super::CICKind::from_hash_md5(hash_str) + } + + #[staticmethod] + pub fn fromValue(value: usize) -> Option { + super::CICKind::from_value(value) + } + + #[getter] + pub fn name(&self) -> &str { + match self { + super::CICKind::CIC_6101 => "CIC_6101", + super::CICKind::CIC_6102_7101 => "CIC_6102_7101", + super::CICKind::CIC_7102 => "CIC_7102", + super::CICKind::CIC_X103 => "CIC_X103", + super::CICKind::CIC_X105 => "CIC_X105", + super::CICKind::CIC_X106 => "CIC_X106", + } + } + } +} diff --git a/src/rs/detect.rs b/src/rs/detect.rs new file mode 100644 index 0000000..8dfeb01 --- /dev/null +++ b/src/rs/detect.rs @@ -0,0 +1,56 @@ +/* SPDX-FileCopyrightText: © 2023 Decompollaborate */ +/* SPDX-License-Identifier: MIT */ + +use crate::cickinds::CICKind; +use crate::utils; + +/// Tries to detect an IPL3 binary. +/// +/// The argument to this function must be exactly the IPL3 binary, stripping the rest of the ROM. +/// +/// ## Arguments +/// +/// * `raw_bytes` - IPL3 binary in big endian format. +/// +/// ## Return +/// * The detected CIC kind, or `None` if was not able to detect the CIC kind. +pub fn detect_cic_raw(raw_bytes: &[u8]) -> Option { + if raw_bytes.len() != 0xFC0 { + return None; + } + + let bytes_hash = utils::get_hash_md5(raw_bytes); + + CICKind::from_hash_md5(&bytes_hash) +} + +/// Tries to detect an IPL3 in a ROM. +/// +/// The argument to this function must be a ROM in big endian format. +/// +/// ## Arguments +/// +/// * `rom_bytes` - ROM binary in big endian format. +/// +/// ## Return +/// +/// * The detected CIC kind, or `None` if was not able to detect the CIC kind. +pub fn detect_cic(rom_bytes: &[u8]) -> Option { + detect_cic_raw(&rom_bytes[0x40..0x1000]) +} + +#[cfg(feature = "python_bindings")] +#[allow(non_snake_case)] +pub(crate) mod python_bindings { + use pyo3::prelude::*; + + #[pyfunction] + pub(crate) fn detectCICRaw(raw_bytes: &[u8]) -> Option { + super::detect_cic_raw(raw_bytes) + } + + #[pyfunction] + pub(crate) fn detectCIC(rom_bytes: &[u8]) -> Option { + super::detect_cic(rom_bytes) + } +} diff --git a/src/rs/lib.rs b/src/rs/lib.rs new file mode 100644 index 0000000..223c953 --- /dev/null +++ b/src/rs/lib.rs @@ -0,0 +1,35 @@ +/* SPDX-FileCopyrightText: © 2023 Decompollaborate */ +/* SPDX-License-Identifier: MIT */ + +mod checksum; +mod cickinds; +mod detect; + +mod utils; + +pub use checksum::*; +pub use cickinds::*; +pub use detect::*; + +#[cfg(feature = "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(()) +} + +#[cfg(test)] +mod tests {} diff --git a/src/rs/utils.rs b/src/rs/utils.rs new file mode 100644 index 0000000..6ab525a --- /dev/null +++ b/src/rs/utils.rs @@ -0,0 +1,35 @@ +/* SPDX-FileCopyrightText: © 2023 Decompollaborate */ +/* SPDX-License-Identifier: MIT */ + +pub(crate) fn read_u32(bytes: &[u8], offset: usize) -> u32 { + if offset % 4 != 0 { + panic!("Unaligned read"); + } + + if offset + 4 > bytes.len() { + panic!("Out of bounds. Offset {:X}, len {:X}", offset, bytes.len()); + } + + /* + match bytes[offset..offset + 4].try_into() { + Ok(bytes) => u32::from_be_bytes(bytes), + Err(_error) => todo!(), + } + */ + + u32::from_be_bytes(bytes[offset..offset + 4].try_into().unwrap()) +} + +pub(crate) fn read_u32_vec(bytes: &[u8], offset: usize, len: usize) -> Vec { + let mut ret = Vec::with_capacity(len); + + for i in 0..len { + ret.push(read_u32(bytes, offset + i * 4)); + } + + ret +} + +pub(crate) fn get_hash_md5(bytes: &[u8]) -> String { + format!("{:x}", md5::compute(bytes)) +} diff --git a/tests/check_recursive.py b/tests/check_recursive.py index ebdbe36..4a43925 100755 --- a/tests/check_recursive.py +++ b/tests/check_recursive.py @@ -59,11 +59,6 @@ def recursePaths(folder: Path) -> int: errors += recursePaths(subpath) continue - if subpath.parts[-2] == "drmario64" and subpath.name == "baserom.cn.z64": - # iQue has a wrong checksum for some reason - print(f"Skipping {subpath}") - continue - romBytes = subpath.read_bytes() romMagic = struct.unpack_from(f">I", romBytes, 0x0)[0]