diff --git a/.github/workflows/c_bindings.yml b/.github/workflows/c_bindings.yml new file mode 100644 index 0000000..0759538 --- /dev/null +++ b/.github/workflows/c_bindings.yml @@ -0,0 +1,129 @@ +name: C bindings + +on: [push, pull_request] + +env: + CARGO_TERM_COLOR: always + +jobs: + test-c-bindings: + name: Test C bindings + runs-on: ubuntu-latest + strategy: + fail-fast: false + + steps: + - name: Checkout repo + uses: actions/checkout@v4 + + - name: Make C test programs + run: make -C bindings/c BUILD_MODE=release + + - name: Test dummies + run: ./bindings/c/tests/test_checksum_dummies.sh + + check_clippy_c_bindings: + name: Check clippy for C bindings + 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: Setup clippy + run: rustup component add clippy + + - name: Run clippy + run: cargo clippy --all-targets --features c_bindings -- -D warnings + + release: + name: Release ${{ matrix.crate-type }} for ${{ matrix.target }} + runs-on: ubuntu-20.04 + strategy: + fail-fast: false + matrix: + target: [x86_64-pc-windows-gnu, x86_64-pc-windows-msvc, x86_64-apple-darwin, x86_64-unknown-linux-musl, x86_64-unknown-linux-gnu] + crate-type: [staticlib] + include: + - target: x86_64-pc-windows-gnu + archive: zip + - target: x86_64-pc-windows-msvc + archive: zip + - target: x86_64-apple-darwin + archive: zip + - target: x86_64-unknown-linux-musl + archive: tar.gz + - target: x86_64-unknown-linux-gnu + archive: tar.gz + + steps: + - name: Checkout repo + uses: actions/checkout@v4 + + - name: Setup Rust toolchain + uses: dtolnay/rust-toolchain@stable + with: + targets: ${{ matrix.target }} + + - name: Build lib + run: cargo rustc --lib --features c_bindings --release --crate-type ${{ matrix.crate-type }} --target ${{ matrix.target }} + + - name: Print built files + run: | + tree target/${{ matrix.target }}/release + + - name: Move files for packaging + run: | + mkdir -p package/lib package/include + cp target/${{ matrix.target }}/release/libipl3checksum.a package/lib/ || cp target/${{ matrix.target }}/release/ipl3checksum.lib package/lib/ + cp -r bindings/c/include/* package/include/ + cp LICENSE package/ipl3checksum.LICENSE + cp README.md package/ipl3checksum.README.md + cp CHANGELOG.md package/ipl3checksum.CHANGELOG.md + tree package + + - name: Package .tar.gz + if: matrix.archive == 'tar.gz' + run: | + cd package && tar -czf ../ipl3checksum-${{ matrix.crate-type }}-${{ matrix.target }}.tar.gz * + + - name: Package .zip + if: matrix.archive == 'zip' + run: | + cd package && zip -r ../ipl3checksum-${{ matrix.crate-type }}-${{ matrix.target }}.zip * + + - name: Upload .tar.gz archive + if: matrix.archive == 'tar.gz' + uses: actions/upload-artifact@v3 + with: + name: ipl3checksum-${{ matrix.crate-type }}-${{ matrix.target }} + path: | + ipl3checksum-${{ matrix.crate-type }}-${{ matrix.target }}.tar.gz + if-no-files-found: error + + - name: Upload .zip archive + if: matrix.archive == 'zip' + uses: actions/upload-artifact@v3 + with: + name: ipl3checksum-${{ matrix.crate-type }}-${{ matrix.target }} + path: | + ipl3checksum-${{ matrix.crate-type }}-${{ matrix.target }}.zip + if-no-files-found: error + + - name: Publish .tar.gz release + uses: softprops/action-gh-release@v1 + if: startsWith(github.ref, 'refs/tags/') && matrix.archive == 'tar.gz' + with: + files: ipl3checksum-${{ matrix.crate-type }}-${{ matrix.target }}.tar.gz + + - name: Publish .zip release + uses: softprops/action-gh-release@v1 + if: startsWith(github.ref, 'refs/tags/') && matrix.archive == 'zip' + with: + files: ipl3checksum-${{ matrix.crate-type }}-${{ matrix.target }}.zip diff --git a/.github/workflows/maturin_upload_pypi.yml b/.github/workflows/maturin_upload_pypi.yml new file mode 100644 index 0000000..8c197c3 --- /dev/null +++ b/.github/workflows/maturin_upload_pypi.yml @@ -0,0 +1,164 @@ +# This file is autogenerated by maturin v1.4.0 +# To update, run +# +# maturin generate-ci github +# +name: Upload to PyPI + +on: + push: + pull_request: + workflow_dispatch: + +permissions: + contents: read + +jobs: + linux: + runs-on: ubuntu-latest + strategy: + matrix: + target: [x86_64, x86, aarch64, armv7, s390x, ppc64le] + fail-fast: false + steps: + - name: Checkout repo + uses: actions/checkout@v4 + + - name: Setup Python + uses: actions/setup-python@v4 + with: + python-version: '3.10' + + - name: Build wheels + uses: PyO3/maturin-action@v1 + with: + target: ${{ matrix.target }} + args: --release --out dist --find-interpreter + sccache: 'true' + manylinux: auto + + - name: Upload wheels + uses: actions/upload-artifact@v3 + with: + name: wheels + path: dist + if-no-files-found: error + + windows: + runs-on: windows-latest + strategy: + matrix: + target: [x64, x86] + fail-fast: false + steps: + - name: Checkout repo + uses: actions/checkout@v4 + + - name: Setup Python + uses: actions/setup-python@v4 + with: + python-version: '3.10' + architecture: ${{ matrix.target }} + + - name: Build wheels + uses: PyO3/maturin-action@v1 + with: + target: ${{ matrix.target }} + args: --release --out dist --find-interpreter + sccache: 'true' + + - name: Upload wheels + uses: actions/upload-artifact@v3 + with: + name: wheels + path: dist + if-no-files-found: error + + macos: + runs-on: macos-latest + strategy: + matrix: + target: [x86_64, aarch64] + steps: + - name: Checkout repo + uses: actions/checkout@v4 + + - name: Setup Python + uses: actions/setup-python@v4 + with: + python-version: '3.10' + + - name: Build wheels + uses: PyO3/maturin-action@v1 + with: + target: ${{ matrix.target }} + args: --release --out dist --find-interpreter + sccache: 'true' + + - name: Upload wheels + uses: actions/upload-artifact@v3 + with: + name: wheels + path: dist + if-no-files-found: error + + sdist: + runs-on: ubuntu-latest + strategy: + fail-fast: false + steps: + - name: Checkout repo + uses: actions/checkout@v4 + + - name: Build sdist + uses: PyO3/maturin-action@v1 + with: + command: sdist + args: --out dist + + - name: Upload sdist + uses: actions/upload-artifact@v3 + with: + name: wheels + path: dist + + check_clippy_python_bindings: + name: Check clippy for C bindings + 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: Setup clippy + run: rustup component add clippy + + - name: Run clippy + run: cargo clippy --all-targets --features python_bindings -- -D warnings + + release: + name: Release + runs-on: ubuntu-latest + if: startsWith(github.ref, 'refs/tags/') + needs: [linux, windows, macos, sdist, check_clippy_python_bindings] + strategy: + fail-fast: false + steps: + - name: Download artifact + uses: actions/download-artifact@v3 + with: + name: wheels + + - name: Publish to PyPI + uses: PyO3/maturin-action@v1 + env: + MATURIN_PYPI_TOKEN: ${{ secrets.pypi_password }} + with: + command: upload + args: --non-interactive --skip-existing * 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..0acd914 --- /dev/null +++ b/.github/workflows/publish_crate.yml @@ -0,0 +1,84 @@ +name: Build and upload Rust crate + +# Build on every branch push, tag push, and pull request change: +on: [push, pull_request] + +jobs: + check_fmt: + name: Check format + 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 + + check_clippy: + name: Check clippy + 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: Setup clippy + run: rustup component add clippy + + - name: Run clippy + run: cargo clippy --all-targets -- -D warnings + + run_tests: + name: Run 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: Run tests + run: cargo test --workspace + + publish: + name: Publish + 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: Build Rust package + run: cargo build --release --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/test_other_repo_c.yml.yml b/.github/workflows/test_other_repo_c.yml.yml new file mode 100644 index 0000000..4d3131f --- /dev/null +++ b/.github/workflows/test_other_repo_c.yml.yml @@ -0,0 +1,53 @@ +name: Test other repo (C) + +# Build on every branch push, tag push, and pull request change: +on: [push, pull_request] + +jobs: + test_other_repo_c_detect: + name: Test other repo detect (C) + runs-on: ubuntu-latest + + strategy: + fail-fast: false + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Get extra dependencies + uses: actions/checkout@v4 + with: + repository: ${{ secrets.SECRETREPO }} + token: ${{ secrets.SECRETTOKEN }} + path: deps_repo + + - name: Build + run: make -C bindings/c BUILD_MODE=release + + - name: Test detection + run: bindings/c/tests/test_detect_other_repo.sh + + test_other_repo_c_checksum_autodetect: + name: Test other repo checksum_autodetect (C) + runs-on: ubuntu-latest + + strategy: + fail-fast: false + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Get extra dependencies + uses: actions/checkout@v4 + with: + repository: ${{ secrets.SECRETREPO }} + token: ${{ secrets.SECRETTOKEN }} + path: deps_repo + + - name: Build + run: make -C bindings/c BUILD_MODE=release + + - name: Test checksum with autodetection + run: bindings/c/tests/test_checksum_autodetect_other_repo.sh diff --git a/.github/workflows/test_other_repo_python.yml b/.github/workflows/test_other_repo_python.yml new file mode 100644 index 0000000..a1212e7 --- /dev/null +++ b/.github/workflows/test_other_repo_python.yml @@ -0,0 +1,61 @@ +name: Test other repo (Python) + +# Build on every branch push, tag push, and pull request change: +on: [push, pull_request] + +jobs: + test_other_repo_py_script: + name: Test other repo (Python script) + runs-on: ubuntu-latest + + strategy: + fail-fast: false + + steps: + - 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 . + + - name: Get extra dependencies + uses: actions/checkout@v4 + with: + repository: ${{ secrets.SECRETREPO }} + token: ${{ secrets.SECRETTOKEN }} + path: deps_repo + + - name: Run repo tests + run: python3 tests/check_recursive.py deps_repo + + test_other_repo_py_cli: + name: Test other repo (Python cli) + runs-on: ubuntu-latest + + strategy: + fail-fast: false + + steps: + - 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 . + + - name: Get extra dependencies + uses: actions/checkout@v4 + with: + repository: ${{ secrets.SECRETREPO }} + token: ${{ secrets.SECRETTOKEN }} + path: deps_repo + + - name: Run repo tests + run: bindings/python/tests/test_check_other_repo.sh 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 deleted file mode 100644 index 9f3aa0e..0000000 --- a/.github/workflows/tests_other_repo.yml +++ /dev/null @@ -1,29 +0,0 @@ -name: Test other repo - -# Build on every branch push, tag push, and pull request change: -on: [push, pull_request] - -jobs: - build_repo: - name: Test other repo - runs-on: ubuntu-latest - - strategy: - fail-fast: false - - steps: - - name: Checkout repository - uses: actions/checkout@v4 - - - name: Install local ipl3checksum - run: pip install . - - - name: Get extra dependencies - uses: actions/checkout@v4 - with: - repository: ${{ secrets.SECRETREPO }} - token: ${{ secrets.SECRETTOKEN }} - path: deps_repo - - - name: Run repo tests - run: python3 tests/check_recursive.py deps_repo diff --git a/.github/workflows/upload_pypi.yml b/.github/workflows/upload_pypi.yml deleted file mode 100644 index 1fe68db..0000000 --- a/.github/workflows/upload_pypi.yml +++ /dev/null @@ -1,38 +0,0 @@ -name: Build and upload to PyPI - -# Build on every branch push, tag push, and pull request change: -on: [push, pull_request] - -jobs: - build_wheel: - name: Build wheel - runs-on: ubuntu-latest - - steps: - - name: Checkout repository - uses: actions/checkout@v4 - - - name: Install build module - run: pip install build - - - name: Build wheel and source - run: python -m build --sdist --wheel --outdir dist/ . - - - uses: actions/upload-artifact@v3 - with: - path: dist/* - - upload_pypi: - needs: [build_wheel] - runs-on: ubuntu-latest - if: github.event_name == 'push' && startsWith(github.ref, 'refs/tags/') - steps: - - uses: actions/download-artifact@v3 - with: - name: artifact - path: dist - - - uses: pypa/gh-action-pypi-publish@v1.8.10 - with: - user: __token__ - password: ${{ secrets.pypi_password }} diff --git a/.gitignore b/.gitignore index 631da74..eee3820 100644 --- a/.gitignore +++ b/.gitignore @@ -166,3 +166,4 @@ cython_debug/ asm/ *.z64 *.elf +.vscode/settings.json diff --git a/CHANGELOG.md b/CHANGELOG.md index 1cce3fd..267c081 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,30 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +## [1.1.0] - 2023-12-22 + +### Added + +- Add Rust support. +- New static methods for `CICKind`. + - `CICKind.fromHashMd5`: Returns a CIC kind based on the passed md5 hash. + - `CICKind.fromName`: Returns a CIC kind based a string representing its name. +- Add C bindings. +- Add support for the IPL3 5101 variant (Used by Aleck 64 games). +- New frontends: + - `check`: Checks if the checksum in the ROM matches the calculated one. + - `detect_cic`: Allows to detect the cic type used by a rom. + - `sum`: Calculates the ipl3 checksum of a rom. + +### 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 ### Added @@ -28,6 +52,7 @@ version of the library. - Initial relase -[unreleased]: https://github.com/olivierlacan/keep-a-changelog/compare/1.1.0...HEAD -[1.0.1]: https://github.com/olivierlacan/keep-a-changelog/compare/1.0.0...1.1.1 +[unreleased]: https://github.com/Decompollaborate/ipl3checksum/compare/main...develop +[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 [1.0.0]: https://github.com/Decompollaborate/ipl3checksum/releases/tag/1.0.0 diff --git a/Cargo.lock b/Cargo.lock new file mode 100644 index 0000000..5460839 --- /dev/null +++ b/Cargo.lock @@ -0,0 +1,308 @@ +# 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", + "thiserror", +] + +[[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 = "thiserror" +version = "1.0.51" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f11c217e1416d6f036b870f14e0413d480dbf28edbee1f877abaf0206af43bb7" +dependencies = [ + "thiserror-impl", +] + +[[package]] +name = "thiserror-impl" +version = "1.0.51" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "01742297787513b79cf8e29d1056ede1313e2420b7b3b15d0a768b4921f549df" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[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..40443be --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,25 @@ +# SPDX-FileCopyrightText: © 2023 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.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", "staticlib", "cdylib"] + +[dependencies] +md5 = "0.7.0" +pyo3 = { version="0.20.0", features = ["extension-module"], optional = true } +thiserror = "1.0.51" + +[features] +c_bindings = [] +python_bindings = ["dep:pyo3"] diff --git a/README.md b/README.md index 127d633..8a9c3ec 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,18 +12,26 @@ [PyPI]: [GitHub contributors]: -A Python library to calculate the IPL3 checksum for N64 ROMs. +A library to calculate the IPL3 checksum for N64 ROMs. + +Written in Rust. Python and C bindings available. ## How to use it? To calculate the checksum of a ROM: ```py +import ipl3checksum + romBytes = # A big endian bytes-like object cickind = ipl3checksum.CICKind.CIC_6102_7101 +# or calculateChecksumAutodetect to let the library guess the correct CIC kind 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,15 +41,19 @@ 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` object or `None`` if was not able to detect +# the CIC kind +print(cickind) ``` ## Features - Supports all 6 retail CIC variants. +- Supports the CIC 5101 variant (used on Aleck 64 games). - 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 +62,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 +79,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 +111,62 @@ 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" +``` + +### C bindings + +This library provides bindings to call this library from C code. They are +available on the [releases](https://github.com/decompals/ipl3checksum/releases) +tab. + +To build said bindings from source, enable the `c_bindings` Rust feature: + +```bash +cargo build --lib --features c_bindings +``` + +Headers are located at [bindings/c/include](bindings/c/include). + +#### Windows executables + +Due to Rust requirements, linking the C bindings of this library when building +a C program adds extra library dependencies. Those libraries are the following: + +```plain_text +-lws2_32 -lntdll -lbcrypt -ladvapi32 -luserenv +``` + +## Examples + +Various examples for the Python bindings are provided in the +[frontends folder](src/ipl3checksum/frontends). + +Those examples are distributed with the Python library as cli tools. Each one +of them can be executed with either `ipl3checksum utilityname` or + `python3 -m ipl3checksum utilityname`, for example `ipl3checksum detect_cic`. + +The list can be checked in runtime with `ipl3checksum --help`. Suboptions for +each tool can be checked with `ipl3checksum utilityname --help`. + +- `check`: Checks if the checksum in the ROM matches the calculated one. +- `detect_cic`: Tries to detect the cic used from the given big endian rom. +- `sum`: Calculates the ipl3 checksum o a given big endian rom, allowing to + optionally update the checksum. + ## Versioning and changelog This library follows [Semantic Versioning](https://semver.org/spec/v2.0.0.html). diff --git a/bindings/c/Makefile b/bindings/c/Makefile new file mode 100644 index 0000000..0c366ad --- /dev/null +++ b/bindings/c/Makefile @@ -0,0 +1,39 @@ +# Either `debug` or `release` +BUILD_MODE ?= debug + +CC := gcc +LIB := ../../target/$(BUILD_MODE)/libipl3checksum.a + +CSTD := -std=c11 +ifeq ($(BUILD_MODE), debug) + CFLAGS ?= -O0 -g3 +else + CFLAGS ?= -Os +endif +IINC := -I include +WARNINGS := -Wall -Wextra -Wshadow -Werror + + +C_BINDINGS_TESTS := $(wildcard tests/test_*.c) +C_BINDINGS_ELFS := $(C_BINDINGS_TESTS:.c=.elf) + +all: $(C_BINDINGS_ELFS) + +clean: + $(RM) -rf $(C_BINDINGS_ELFS) tests/*.elf + +.PHONY: all clean +.DEFAULT_GOAL := all + +CARGO_FLAGS ?= +ifneq ($(BUILD_MODE), debug) + CARGO_FLAGS += --release +endif +$(shell cargo build --lib --features c_bindings $(CARGO_FLAGS)) + + +%.elf: %.c $(LIB) tests/utils.c + $(CC) $(CSTD) $(CFLAGS) $(IINC) $(WARNINGS) -o $@ tests/utils.c $< -L ../../target/$(BUILD_MODE) -Wl,-Bstatic -l ipl3checksum -Wl,-Bdynamic + +# Print target for debugging +print-% : ; $(info $* is a $(flavor $*) variable set to [$($*)]) @true diff --git a/bindings/c/include/ipl3checksum.h b/bindings/c/include/ipl3checksum.h new file mode 100644 index 0000000..5de8b77 --- /dev/null +++ b/bindings/c/include/ipl3checksum.h @@ -0,0 +1,12 @@ +#ifndef IPL3CHECKSUM_H +#define IPL3CHECKSUM_H +#pragma once + +#include "ipl3checksum/error.h" +#include "ipl3checksum/cickinds.h" +#include "ipl3checksum/checksum.h" +#include "ipl3checksum/detect.h" +#include "ipl3checksum/utils.h" +#include "ipl3checksum/version.h" + +#endif diff --git a/bindings/c/include/ipl3checksum/checksum.h b/bindings/c/include/ipl3checksum/checksum.h new file mode 100644 index 0000000..944752b --- /dev/null +++ b/bindings/c/include/ipl3checksum/checksum.h @@ -0,0 +1,69 @@ +#ifndef IPL3CHECKSUM_CHECKSUM_H +#define IPL3CHECKSUM_CHECKSUM_H +#pragma once + +#include +#include + +#include "error.h" +#include "cickinds.h" + +#ifdef __cplusplus +extern "C" +{ +#endif + +/** + * Calculates the checksum required by an official CIC of a N64 ROM. + * + * ## Arguments + * + * * `dst_checksum0` - Pointer where the first word of the calculated checksum will be placed. + * * `dst_checksum1` - Pointer where the second word of the calculated checksum will be placed. + * * `rom_bytes_len` - Bytes length of the input `rom_bytes`. + * * `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 + * + * * `Ipl3Checksum_Error` indicating either a successful execution or the cause for failing. + * If execution fails then `dst_checksum0` and `dst_checksum1` are left untouched. + */ +Ipl3Checksum_Error ipl3checksum_calculate_checksum( + uint32_t *dst_checksum0, + uint32_t *dst_checksum1, + size_t rom_bytes_len, + const uint8_t *rom_bytes, + Ipl3Checksum_CICKind kind +); + +/** + * 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 an error will be returned. + * + * ## Arguments + * + * * `dst_checksum0` - Pointer where the first word of the calculated checksum will be placed. + * * `dst_checksum1` - Pointer where the second word of the calculated checksum will be placed. + * * `rom_bytes_len` - Bytes length of the input `rom_bytes`. + * * `rom_bytes` - The bytes of the N64 ROM in big endian format. It must have a minimum size of 0x101000 bytes. + * + * ## Return + * + * * `Ipl3Checksum_Error` indicating either a successful execution or the cause for failing. + * If execution fails then `dst_checksum0` and `dst_checksum1` are left untouched. + */ +Ipl3Checksum_Error ipl3checksum_calculate_checksum_autodetect( + uint32_t *dst_checksum0, + uint32_t *dst_checksum1, + size_t rom_bytes_len, + const uint8_t *rom_bytes +); + +#ifdef __cplusplus +} +#endif + +#endif diff --git a/bindings/c/include/ipl3checksum/cickinds.h b/bindings/c/include/ipl3checksum/cickinds.h new file mode 100644 index 0000000..bed923c --- /dev/null +++ b/bindings/c/include/ipl3checksum/cickinds.h @@ -0,0 +1,62 @@ +#ifndef IPL3CHECKSUM_CICKINDS_H +#define IPL3CHECKSUM_CICKINDS_H +#pragma once + +#include +#include + +#include "error.h" + +#ifdef __cplusplus +extern "C" +{ +#endif + +/* This needs to be synced with the Rust equivalent in `src/rs/cickinds.rs` */ +typedef enum Ipl3Checksum_CICKind { + Ipl3Checksum_CICKind_CIC_6101, + Ipl3Checksum_CICKind_CIC_6102_7101, + Ipl3Checksum_CICKind_CIC_7102, + Ipl3Checksum_CICKind_CIC_X103, // Both 6103 and 7103 + // 6104/7104 does not exist + Ipl3Checksum_CICKind_CIC_X105, // Both 6105 and 7105 + Ipl3Checksum_CICKind_CIC_X106, // Both 6106 and 7106 + + Ipl3Checksum_CICKind_CIC_5101, // Aleck 64 +} Ipl3Checksum_CICKind; + +uint32_t ipl3checksum_cickind_get_seed(Ipl3Checksum_CICKind self); + +uint32_t ipl3checksum_cickind_get_magic(Ipl3Checksum_CICKind self); + +/** + * Returns the md5 hash for the specified CIC kind. + * + * If no errors happen (return is an `Ipl3Checksum_Error_Okay`), then the hash + * is stored on `dst_hash`. + * This string is dynamically allocated by the library and it should be freed + * (by passing it to `ipl3checksum_free_string`) to avoid memory leaks. + */ +Ipl3Checksum_Error ipl3checksum_cickind_get_hash_md5(Ipl3Checksum_CICKind self, char **dst_hash); + +Ipl3Checksum_Error ipl3checksum_cickind_from_hash_md5(Ipl3Checksum_CICKind *kind_dst, const char *hash_str); + +/** + * Returns an human readable name for the specified CIC kind. + * + * If no errors happen (return is an `Ipl3Checksum_Error_Okay`), then the name + * is stored on `dst_name`. + * This string is dynamically allocated by the library and it should be freed + * (by passing it to `ipl3checksum_free_string`) to avoid memory leaks. + */ +Ipl3Checksum_Error ipl3checksum_cickind_get_name(Ipl3Checksum_CICKind self, char **dst_name); + +Ipl3Checksum_Error ipl3checksum_cickind_from_name(Ipl3Checksum_CICKind *kind_dst, const char *name); + +Ipl3Checksum_Error ipl3checksum_cickind_from_value(Ipl3Checksum_CICKind *kind_dst, size_t value); + +#ifdef __cplusplus +} +#endif + +#endif diff --git a/bindings/c/include/ipl3checksum/detect.h b/bindings/c/include/ipl3checksum/detect.h new file mode 100644 index 0000000..06d2eb0 --- /dev/null +++ b/bindings/c/include/ipl3checksum/detect.h @@ -0,0 +1,65 @@ +#ifndef IPL3CHECKSUM_DETECT_H +#define IPL3CHECKSUM_DETECT_H +#pragma once + +#include +#include + +#include "error.h" +#include "cickinds.h" + +#ifdef __cplusplus +extern "C" +{ +#endif + +/** + * Tries to detect an IPL3 binary. + * + * The argument to this function must be exactly the IPL3 binary, meaning the + * binary size must match exactly the one of an IPL3 binary. + * + * ## Arguments + * + * * `dst_kind` - Pointer where the detected kind will be set to. + * * `raw_bytes_len` - Bytes length of the input `raw_bytes`. + * * `raw_bytes` - Bytes of an IPL3 binary in big endian format. + * + * ## Return + * + * * `Ipl3Checksum_Error` indicating either a successful execution or the cause + * for failing. If execution fails then `dst_kind` is left untouched. + */ +Ipl3Checksum_Error ipl3checksum_detect_cic_raw( + Ipl3Checksum_CICKind *dst_kind, + size_t raw_bytes_len, + const uint8_t *raw_bytes +); + +/** + * Tries to detect an IPL3 in a ROM. + * + * The argument to this function must be a ROM in big endian format. + * + * ## Arguments + * + * * `dst_kind` - Pointer where the detected kind will be set to. + * * `raw_bytes_len` - Bytes length of the input `rom_bytes`. + * * `rom_bytes` - ROM binary in big endian format. + * + * ## Return + * + * * `Ipl3Checksum_Error` indicating either a successful execution or the cause + * for failing. If execution fails then `dst_kind` is left untouched. + */ +Ipl3Checksum_Error ipl3checksum_detect_cic( + Ipl3Checksum_CICKind *dst_kind, + size_t rom_bytes_len, + const uint8_t *rom_bytes +); + +#ifdef __cplusplus +} +#endif + +#endif diff --git a/bindings/c/include/ipl3checksum/error.h b/bindings/c/include/ipl3checksum/error.h new file mode 100644 index 0000000..d82654d --- /dev/null +++ b/bindings/c/include/ipl3checksum/error.h @@ -0,0 +1,70 @@ +#ifndef IPL3CHECKSUM_ERROR_H +#define IPL3CHECKSUM_ERROR_H +#pragma once + +#include + +#ifdef __cplusplus +extern "C" +{ +#endif + +/* This needs to be synced with the Rust equivalent in `src/rs/error.rs` */ +typedef enum Ipl3Checksum_Error_Tag { + Ipl3Checksum_Error_Okay, + Ipl3Checksum_Error_NullPointer, + Ipl3Checksum_Error_StringConversion, + + Ipl3Checksum_Error_UnalignedRead, + Ipl3Checksum_Error_ByteConversion, + Ipl3Checksum_Error_OutOfBounds, + Ipl3Checksum_Error_BufferNotBigEnough, + Ipl3Checksum_Error_BufferSizeIsWrong, + Ipl3Checksum_Error_UnableToDetectCIC, +} Ipl3Checksum_Error_Tag; + +/** + * Most functions of this library return a Ipl3Checksum_Error object which + * indicates if the function ended successfully or if it failed (and why it + * failed). If a function is expected to return a value on success, then said + * value will be set via argument pointers. + * + * A successful execution has the `.tag` member set to `Ipl3Checksum_Error_Okay`, + * everything else is considered an error. + * + * If an error ocurred then the argument dst pointers will be left untouched. + * + * The `.payload` union member may have extra information on why the function + * call failed. This information is set only for a few selected + * `Ipl3Checksum_Error_Tag` tags. + */ +typedef struct Ipl3Checksum_Error { + Ipl3Checksum_Error_Tag tag; + union Ipl3Checksum_Error_Payload { + struct Ipl3Checksum_Error_Payload_UnalignedRead { + size_t offset; + } UnalignedRead; + struct Ipl3Checksum_Error_Payload_ByteConversion { + size_t offset; + } ByteConversion; + struct Ipl3Checksum_Error_Payload_OutOfBounds { + size_t offset; + size_t requested_bytes; + size_t buffer_len; + } OutOfBounds; + struct Ipl3Checksum_Error_Payload_BufferNotBigEnough { + size_t buffer_len; + size_t expected_len; + } BufferNotBigEnough; + struct Ipl3Checksum_Error_Payload_BufferSizeIsWrong { + size_t buffer_len; + size_t expected_len; + } BufferSizeIsWrong; + } payload; +} Ipl3Checksum_Error; + +#ifdef __cplusplus +} +#endif + +#endif diff --git a/bindings/c/include/ipl3checksum/utils.h b/bindings/c/include/ipl3checksum/utils.h new file mode 100644 index 0000000..300282b --- /dev/null +++ b/bindings/c/include/ipl3checksum/utils.h @@ -0,0 +1,12 @@ +#ifndef IPL3CHECKSUM_UTILS_H +#define IPL3CHECKSUM_UTILS_H +#pragma once + +#include "error.h" + +/** + * Free a string returned by the ipl3checksum library. + */ +Ipl3Checksum_Error ipl3checksum_free_string(char *s); + +#endif diff --git a/bindings/c/include/ipl3checksum/version.h b/bindings/c/include/ipl3checksum/version.h new file mode 100644 index 0000000..a31d8a5 --- /dev/null +++ b/bindings/c/include/ipl3checksum/version.h @@ -0,0 +1,15 @@ +#ifndef IPL3CHECKSUM_VERSION_H +#define IPL3CHECKSUM_VERSION_H +#pragma once + +#include + +extern const int32_t ipl3checksum_version_major; +extern const int32_t ipl3checksum_version_minor; +extern const int32_t ipl3checksum_version_patch; + +extern const char *const ipl3checksum_version_str; + +extern const char *const ipl3checksum_version_author; + +#endif diff --git a/bindings/c/tests/test_checksum.c b/bindings/c/tests/test_checksum.c new file mode 100644 index 0000000..c98704a --- /dev/null +++ b/bindings/c/tests/test_checksum.c @@ -0,0 +1,89 @@ +/* SPDX-FileCopyrightText: © 2023 Decompollaborate */ +/* SPDX-License-Identifier: MIT */ + +#include "ipl3checksum.h" + +#include +#include + +#include "utils.h" + +bool get_cic_kind(Ipl3Checksum_CICKind *dst_kind, const char *cic_kind_name) { + fprintf(stderr, "Passed CIC kind: '%s'\n", cic_kind_name); + + Ipl3Checksum_Error err = ipl3checksum_cickind_from_name(dst_kind, cic_kind_name); + + if (err.tag != Ipl3Checksum_Error_Okay) { + fprintf(stderr, "Passed CIC kind was not valid: %s\n", get_ipl3checksum_error_str(err)); + return false; + } + + if (!eprint_cickind_name(*dst_kind)) { + return false; + } + + return true; +} + +void print_usage(int argc, char *argv[]) { + (void)argc; + + fprintf(stderr, "Usage: %s bin_file cic_kind\n", argv[0]); + fprintf(stderr, "\n"); + fprintf(stderr, "This programs computes the ipl3 checksum of a big endian binary file with a given cic kind\n"); +} + +int main(int argc, char *argv[]) { + int ret = 0; + + if (argc < 3) { + print_usage(argc, argv); + return -1; + } + + fprintf(stderr, "Running ipl3checksum version %s\n", ipl3checksum_version_str); + + const char *bin_path = argv[1]; + const char *cic_kind_name = argv[2]; + + size_t bin_size = 0; + uint8_t *bin = read_binary_file(bin_path, &bin_size); + assert(bin_size > 0); + assert(bin != NULL); + + Ipl3Checksum_CICKind kind; + if (!get_cic_kind(&kind, cic_kind_name)) { + ret++; + goto cleanup; + } + + uint32_t expected_checksum0 = read_be_word(bin, 0x10); + uint32_t expected_checksum1 = read_be_word(bin, 0x14); + + fprintf(stderr, "Expected checksum: %08X %08X\n", expected_checksum0, expected_checksum1); + + { + uint32_t checksum0; + uint32_t checksum1; + + Ipl3Checksum_Error err = ipl3checksum_calculate_checksum(&checksum0, &checksum1, bin_size, bin, kind); + + if (err.tag == Ipl3Checksum_Error_Okay) { + fprintf(stderr, "Computed checksum: %08X %08X\n", checksum0, checksum1); + if ((checksum0 == expected_checksum0) && (checksum1 == expected_checksum1)) { + fprintf(stderr, "Checksum matches\n"); + } else { + fprintf(stderr, "Checksum doesn't match\n"); + ret++; + } + } else { + fprintf(stderr, "Error trying to compute the checksum: %s\n", get_ipl3checksum_error_str(err)); + ret++; + } + } + +cleanup: + free(bin); + + return ret; +} diff --git a/bindings/c/tests/test_checksum_autodetect.c b/bindings/c/tests/test_checksum_autodetect.c new file mode 100644 index 0000000..5a4d121 --- /dev/null +++ b/bindings/c/tests/test_checksum_autodetect.c @@ -0,0 +1,64 @@ +/* SPDX-FileCopyrightText: © 2023 Decompollaborate */ +/* SPDX-License-Identifier: MIT */ + +#include "ipl3checksum.h" + +#include +#include + +#include "utils.h" + +void print_usage(int argc, char *argv[]) { + (void)argc; + + fprintf(stderr, "Usage: %s bin_file\n", argv[0]); + fprintf(stderr, "\n"); + fprintf(stderr, "This programs computes the ipl3 checksum of a big endian binary file by guessing its cic kind\n"); +} + +int main(int argc, char *argv[]) { + int ret = 0; + + if (argc < 2) { + print_usage(argc, argv); + return -1; + } + + fprintf(stderr, "Running ipl3checksum version %s\n", ipl3checksum_version_str); + + const char *bin_path = argv[1]; + + size_t bin_size = 0; + uint8_t *bin = read_binary_file(bin_path, &bin_size); + assert(bin_size > 0); + assert(bin != NULL); + + uint32_t expected_checksum0 = read_be_word(bin, 0x10); + uint32_t expected_checksum1 = read_be_word(bin, 0x14); + + fprintf(stderr, "Expected checksum: %08X %08X\n", expected_checksum0, expected_checksum1); + + { + uint32_t checksum0; + uint32_t checksum1; + + Ipl3Checksum_Error err = ipl3checksum_calculate_checksum_autodetect(&checksum0, &checksum1, bin_size, bin); + + if (err.tag == Ipl3Checksum_Error_Okay) { + fprintf(stderr, "Computed checksum: %08X %08X\n", checksum0, checksum1); + if ((checksum0 == expected_checksum0) && (checksum1 == expected_checksum1)) { + fprintf(stderr, "Checksum matches\n"); + } else { + fprintf(stderr, "Checksum doesn't match\n"); + ret++; + } + } else { + fprintf(stderr, "Error trying to compute the checksum: %s\n", get_ipl3checksum_error_str(err)); + ret++; + } + } + + free(bin); + + return ret; +} diff --git a/bindings/c/tests/test_checksum_autodetect_other_repo.sh b/bindings/c/tests/test_checksum_autodetect_other_repo.sh new file mode 100755 index 0000000..3a8a79b --- /dev/null +++ b/bindings/c/tests/test_checksum_autodetect_other_repo.sh @@ -0,0 +1,22 @@ +#! /usr/bin/env bash + +set -e + +# https://stackoverflow.com/a/17902999/6292472 +files=$(shopt -s nullglob dotglob; echo deps_repo/*) +if (( ${#files} )) +then + echo "deps_repo contains files" +else + echo "deps_repo is empty (or does not exist or is a file)" + exit 1 +fi + +for i in $(find deps_repo -type f -name "*.z64"); do + [ -f "$i" ] || break + echo "Processing:" $i + + ./bindings/c/tests/test_checksum_autodetect.elf $i + + echo +done diff --git a/bindings/c/tests/test_checksum_dummies.sh b/bindings/c/tests/test_checksum_dummies.sh new file mode 100755 index 0000000..3877be3 --- /dev/null +++ b/bindings/c/tests/test_checksum_dummies.sh @@ -0,0 +1,15 @@ +#! /usr/bin/env bash + +set -e + +for i in tests/dummytests/*/*; do + [ -f "$i" ] || break + echo "Processing:" $i + + # Get CIC kind based on folder's name + CIC_KIND=$(basename $(dirname $i)) + echo "CIC kind" $CIC_KIND + + ./bindings/c/tests/test_checksum.elf $i $CIC_KIND + echo +done diff --git a/bindings/c/tests/test_detect.c b/bindings/c/tests/test_detect.c new file mode 100644 index 0000000..6027436 --- /dev/null +++ b/bindings/c/tests/test_detect.c @@ -0,0 +1,60 @@ +/* SPDX-FileCopyrightText: © 2023 Decompollaborate */ +/* SPDX-License-Identifier: MIT */ + +#include "ipl3checksum.h" + +#include +#include + +#include "utils.h" + +bool detect(size_t bin_size, const uint8_t *bin) { + + Ipl3Checksum_CICKind kind; + Ipl3Checksum_Error err = ipl3checksum_detect_cic(&kind, bin_size, bin); + + if (err.tag != Ipl3Checksum_Error_Okay) { + fprintf(stderr, "Error trying to detect the cic: %s\n", get_ipl3checksum_error_str(err)); + return false; + } + + if (!eprint_cickind_name(kind)) { + return false; + } + + return true; +} + +void print_usage(int argc, char *argv[]) { + (void)argc; + + fprintf(stderr, "Usage: %s bin_file\n", argv[0]); + fprintf(stderr, "\n"); + fprintf(stderr, "This programs detects the cic kind of a given big endian rom\n"); +} + +int main(int argc, char *argv[]) { + int ret = 0; + + if (argc < 2) { + print_usage(argc, argv); + return -1; + } + + fprintf(stderr, "Running ipl3checksum version %s\n", ipl3checksum_version_str); + + const char *bin_path = argv[1]; + + size_t bin_size = 0; + uint8_t *bin = read_binary_file(bin_path, &bin_size); + assert(bin_size > 0); + assert(bin != NULL); + + if (!detect(bin_size, bin)) { + ret++; + } + + free(bin); + + return ret; +} diff --git a/bindings/c/tests/test_detect_other_repo.sh b/bindings/c/tests/test_detect_other_repo.sh new file mode 100755 index 0000000..c05f128 --- /dev/null +++ b/bindings/c/tests/test_detect_other_repo.sh @@ -0,0 +1,22 @@ +#! /usr/bin/env bash + +set -e + +# https://stackoverflow.com/a/17902999/6292472 +files=$(shopt -s nullglob dotglob; echo deps_repo/*) +if (( ${#files} )) +then + echo "deps_repo contains files" +else + echo "deps_repo is empty (or does not exist or is a file)" + exit 1 +fi + +for i in $(find deps_repo -type f -name "*.z64"); do + [ -f "$i" ] || break + echo "Processing:" $i + + ./bindings/c/tests/test_detect.elf $i + + echo +done diff --git a/bindings/c/tests/utils.c b/bindings/c/tests/utils.c new file mode 100644 index 0000000..dfeabc5 --- /dev/null +++ b/bindings/c/tests/utils.c @@ -0,0 +1,76 @@ +/* SPDX-FileCopyrightText: © 2023 Decompollaborate */ +/* SPDX-License-Identifier: MIT */ + +#include "utils.h" + +#include +#include +#include +#include + +uint8_t *read_binary_file(const char *path, size_t *size) { + assert(path != NULL); + assert(size != NULL); + + fprintf(stderr, "Reading file %s\n", path); + + FILE *f = fopen(path, "rb"); + if (f == NULL) { + return NULL; + } + + fseek(f, 0, SEEK_END); + *size = ftell(f); + fseek(f, 0, SEEK_SET); + + uint8_t *data = malloc(*size * sizeof(uint8_t)); + if (data == NULL) { + fclose(f); + return NULL; + } + + size_t count = fread(data, sizeof(uint8_t), *size, f); + if (count != *size) { + free(data); + fclose(f); + return NULL; + } + + fclose(f); + return data; +} + +uint32_t read_be_word(const uint8_t *src, size_t offset) { + return (src[offset] << 24) | (src[offset+1] << 16) | (src[offset+2] << 8) | (src[offset+3] << 0); +} + +const char *const ipl3checksum_error_str[] = { + [Ipl3Checksum_Error_Okay] = "Okay", + [Ipl3Checksum_Error_UnalignedRead] = "UnalignedRead", + [Ipl3Checksum_Error_ByteConversion] = "ByteConversion", + [Ipl3Checksum_Error_OutOfBounds] = "OutOfBounds", + [Ipl3Checksum_Error_NullPointer] = "NullPointer", + [Ipl3Checksum_Error_BufferNotBigEnough] = "BufferNotBigEnough", + [Ipl3Checksum_Error_BufferSizeIsWrong] = "BufferSizeIsWrong", + [Ipl3Checksum_Error_UnableToDetectCIC] = "UnableToDetectCIC", + [Ipl3Checksum_Error_StringConversion] = "StringConversion", +}; + +const char *get_ipl3checksum_error_str(Ipl3Checksum_Error error) { + return ipl3checksum_error_str[error.tag]; +} + +bool eprint_cickind_name(Ipl3Checksum_CICKind kind) { + char *kind_name; + Ipl3Checksum_Error kind_name_ok = ipl3checksum_cickind_get_name(kind, &kind_name); + + if (kind_name_ok.tag != Ipl3Checksum_Error_Okay) { + fprintf(stderr, "Failed to get cic kind's name: %s\n", get_ipl3checksum_error_str(kind_name_ok)); + return false; + } + + fprintf(stderr, "CIC kind name: '%s'\n", kind_name); + + ipl3checksum_free_string(kind_name); + return true; +} diff --git a/bindings/c/tests/utils.h b/bindings/c/tests/utils.h new file mode 100644 index 0000000..b5486d8 --- /dev/null +++ b/bindings/c/tests/utils.h @@ -0,0 +1,22 @@ +/* SPDX-FileCopyrightText: © 2023 Decompollaborate */ +/* SPDX-License-Identifier: MIT */ + +#ifndef TESTS_UTILS_H +#define TESTS_UTILS_H +#pragma once + +#include +#include +#include + +#include "ipl3checksum.h" + +uint8_t *read_binary_file(const char *path, size_t *size); + +uint32_t read_be_word(const uint8_t *src, size_t offset); + +const char *get_ipl3checksum_error_str(Ipl3Checksum_Error error); + +bool eprint_cickind_name(Ipl3Checksum_CICKind kind); + +#endif diff --git a/bindings/python/tests/test_check_other_repo.sh b/bindings/python/tests/test_check_other_repo.sh new file mode 100755 index 0000000..c41b109 --- /dev/null +++ b/bindings/python/tests/test_check_other_repo.sh @@ -0,0 +1,22 @@ +#! /usr/bin/env bash + +set -e + +# https://stackoverflow.com/a/17902999/6292472 +files=$(shopt -s nullglob dotglob; echo deps_repo/*) +if (( ${#files} )) +then + echo "deps_repo contains files" +else + echo "deps_repo is empty (or does not exist or is a file)" + exit 1 +fi + +for i in $(find deps_repo -type f -name "*.z64"); do + [ -f "$i" ] || break + echo "Processing:" $i + + ipl3checksum check $i + + echo +done diff --git a/notes/check_5101.py b/notes/check_5101.py new file mode 100755 index 0000000..4e63748 --- /dev/null +++ b/notes/check_5101.py @@ -0,0 +1,144 @@ +#!/usr/bin/env python3 + +# SPDX-License-Identifier: CC0-1.0 + +from __future__ import annotations + +import argparse +from pathlib import Path +import struct + +parser = argparse.ArgumentParser() +parser.add_argument("rom") + +args = parser.parse_args() +romPathArg = args.rom + +romPath = Path(romPathArg) + +romBytes = romPath.read_bytes() + +def u32(value: int) -> int: + value = value & 0xFFFFFFFF + return value + +def readWordFromRam(romWords: list[int], entrypointRam: int, ramAddr: int) -> int: + offset = ramAddr - entrypointRam + 0x1000 + assert offset < 0x101000 + index = offset // 4 + assert index >= 0 + word = romWords[index] + return word + + +# Seems to be a fork of 6103 +def checksumfunc(romBytes: bytes, initial_s6 = 0xAC): + byteCount = len(romBytes) + assert byteCount > 0x101000, f"0x{byteCount:X}" + wordCount = byteCount // 4 + romWords = list(struct.unpack(f">{wordCount}I", romBytes)) + + s6 = initial_s6 + + a0 = romWords[8//4] - 0x100000 + entrypointRam = a0 + + at = 0x6C078965 + lo = s6 * at + + v1 = 0x80000400 + + ra = 0x100000 + + #v1 = 0 + #t0 = 0 + + #t1 = a0 + + #t5 = 0x20 + + v0 = u32(lo) + + v0 += 1 + + a3 = v0 + t2 = v0 + t3 = v0 + s0 = v0 + a2 = v0 + t4 = v0 + + if a0 == v1: + ra = 0x3FE000 + + v1 = 0 + + t0 = 0 + t1 = a0 + t5 = 0x20 + + # poor man's do while + LA40005F0_loop = True + while LA40005F0_loop: + # v0 = *t1 + v0 = readWordFromRam(romWords, entrypointRam, t1) + + v1 = u32(a3 + v0) + + at = u32(v1) < u32(a3) + + a1 = v1 + # if (at == 0) goto LA4000608; + + if at != 0: + t2 = u32(t2 + 0x1) + + # LA4000608 + v1 = v0 & 0x1F + t7 = u32(t5 - v1) + + + t8 = u32(v0 >> t7) + t6 = u32(v0 << v1) + + a0 = t6 | t8 + at = u32(a2) < u32(v0) + a3 = a1 + + t3 = t3 ^ v0 + + s0 = 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: + t0 = u32(t0 + 0x4) + t7 = v0 ^ s0 + t1 = u32(t1 + 0x4) + + + t4 = u32(t7 + t4) + # if (t0 != ra) goto LA40005F0; + if t0 == ra: + LA40005F0_loop = False + + + t6 = a3 ^ t2 + a3 = u32(t6 + t3) + t8 = s0 ^ a2 + s0 = u32(t8 + t4) + + return (a3, s0) + + +v1, v2 = checksumfunc(romBytes) +print(f"{v1:08X}") +print(f"{v2:08X}") diff --git a/notes/checksum.c b/notes/checksum.c index 138fd87..4535206 100644 --- a/notes/checksum.c +++ b/notes/checksum.c @@ -26,6 +26,9 @@ uint32_t getSeed(uint32_t cic) { case 6106: case 7106: return 0x85; + + case 5101: + return 0xAC; } } @@ -44,6 +47,7 @@ uint32_t getMagic(uint32_t cic) { case 7103: case 6106: case 7106: + case 5101: return 0x6C078965; } } @@ -64,6 +68,7 @@ void calculateChecksum(const uint8_t *rom, uint32_t cic, uint32_t *dst1, uint32_ switch (cic) { case 6103: case 7103: + case 5101: a0 -= 0x100000; break; @@ -104,6 +109,14 @@ void calculateChecksum(const uint8_t *rom, uint32_t cic, uint32_t *dst1, uint32_ a2 = v0; t4 = v0; + switch (cic) { + case 5101: + if (a0 == 0x80000400) { + ra = 0x3FE000; + } + break; + } + do { /* lw $v0, 0x0($t1) */ v0 = readWord(rom, t1 - entrypointRam + 0x1000); @@ -170,6 +183,7 @@ void calculateChecksum(const uint8_t *rom, uint32_t cic, uint32_t *dst1, uint32_ switch (cic) { case 6103: case 7103: + case 5101: t6 = a3 ^ t2; a3 = t6 + t3; t8 = s0 ^ a2; diff --git a/pyproject.toml b/pyproject.toml index 85329df..130ee05 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -3,25 +3,38 @@ [project] name = "ipl3checksum" -version = "1.0.1" +# Version should be synced with src/ipl3checksum/__init__.py, Cargo.toml and src/rs/version.rs +version = "1.1.0" description = "Library to calculate the IPL3 checksum for N64 ROMs" readme = "README.md" requires-python = ">=3.7" -dynamic = ["dependencies"] +authors = [ + { name="Anghelo Carvajal", email="angheloalf95@gmail.com" }, +] +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" +ipl3checksum = "ipl3checksum.__main__:ipl3checksumMain" [tool.cibuildwheel] 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..0a7b8d5 100644 --- a/src/ipl3checksum/__init__.py +++ b/src/ipl3checksum/__init__.py @@ -5,16 +5,11 @@ from __future__ import annotations -__version_info__: tuple[int, int, int] = (1, 0, 1) +# Version should be synced with pyproject.toml, Cargo.toml and src/rs/version.rs +__version_info__: tuple[int, int, int] = (1, 1, 0) __version__ = ".".join(map(str, __version_info__)) __author__ = "Decompollaborate" -from . import utils as utils +from .ipl3checksum import * -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 . import frontends as frontends diff --git a/src/ipl3checksum/__main__.py b/src/ipl3checksum/__main__.py index a625150..5ab0ea6 100644 --- a/src/ipl3checksum/__main__.py +++ b/src/ipl3checksum/__main__.py @@ -7,8 +7,23 @@ import argparse -from .frontends import climain +import ipl3checksum + + +def ipl3checksumMain(): + parser = argparse.ArgumentParser(description="Interface to call any of the ipl3checksum's CLI utilities", prog="ipl3checksum") + + parser.add_argument("-V", "--version", action="version", version=f"%(prog)s {ipl3checksum.__version__}") + + subparsers = parser.add_subparsers(description="action", help="The CLI utility to run", required=True) + + ipl3checksum.frontends.check.addSubparser(subparsers) + ipl3checksum.frontends.detect_cic.addSubparser(subparsers) + ipl3checksum.frontends.sum.addSubparser(subparsers) + + args = parser.parse_args() + args.func(args) if __name__ == "__main__": - climain.ipl3checksumMain() + ipl3checksumMain() 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..63c3358 --- /dev/null +++ b/src/ipl3checksum/cickinds.pyi @@ -0,0 +1,54 @@ +#!/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 + + CIC_5101: CICKind # Aleck 64 + + + 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: + ... + + @property + def name(self) -> str: + ... + + @staticmethod + def fromName(name: str) -> CICKind|None: + ... + + @staticmethod + def validNames() -> list[str]: + ... + + @staticmethod + def fromValue(value: int) -> CICKind|None: + ... diff --git a/src/ipl3checksum/detect.py b/src/ipl3checksum/detect.pyi similarity index 68% rename from src/ipl3checksum/detect.py rename to src/ipl3checksum/detect.pyi index cea20b5..b0ab4ba 100644 --- a/src/ipl3checksum/detect.py +++ b/src/ipl3checksum/detect.pyi @@ -5,14 +5,12 @@ 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. - The argument to this function must be exactly the IPL3 binary, stripping the rest of the ROM. + The argument to this function must be exactly the IPL3 binary. Args: rawBytes (bytes): IPL3 binary in big endian format. @@ -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/frontends/__init__.py b/src/ipl3checksum/frontends/__init__.py index 4104677..d6fa356 100644 --- a/src/ipl3checksum/frontends/__init__.py +++ b/src/ipl3checksum/frontends/__init__.py @@ -6,4 +6,6 @@ from __future__ import annotations -from . import climain as climain +from . import check as check +from . import detect_cic as detect_cic +from . import sum as sum diff --git a/src/ipl3checksum/frontends/check.py b/src/ipl3checksum/frontends/check.py new file mode 100644 index 0000000..3c1facc --- /dev/null +++ b/src/ipl3checksum/frontends/check.py @@ -0,0 +1,62 @@ +#!/usr/bin/env python3 + +# SPDX-FileCopyrightText: © 2023 Decompollaborate +# SPDX-License-Identifier: MIT + +from __future__ import annotations + +import argparse +from pathlib import Path +import struct + +import ipl3checksum + +def doCheck(romBytes: bytes, kindName: str | None) -> int: + if kindName is None: + # Detect kind if none was specified by the user + kind = ipl3checksum.detectCIC(romBytes) + if kind is None: + print(f"Unable to detect CIC kind") + return 1 + print(f"Detected kind is '{kind.name}'") + else: + kind = ipl3checksum.CICKind.fromName(kindName) + if kind is None: + print(f"Invalid choice for cic kind. Valid choices: {ipl3checksum.CICKind.validNames()}") + return 1 + + ogChk0, ogChk1 = struct.unpack_from(f">II", romBytes, 0x10) + print(f"Checksum in ROM: {ogChk0:08X} {ogChk1:08X}") + + checksum = ipl3checksum.calculateChecksum(romBytes, kind) + if checksum is None: + print(f"Unable to calculate checksum") + return 1 + + chk0, chk1 = checksum + print(f"Calculated checksum: {chk0:08X} {chk1:08X}") + + if chk0 != ogChk0 or chk1 != ogChk1: + print(f"Checksum doesn't match") + return 1 + + print("Checksum matches") + return 0 + + +def processArguments(args: argparse.Namespace): + romPath: Path = args.rom_path + kindName: str|None = args.kind + + romBytes = romPath.read_bytes() + + exit(doCheck(romBytes, kindName)) + +def addSubparser(subparser: argparse._SubParsersAction[argparse.ArgumentParser]): + parser = subparser.add_parser("check", help="Checks if the checksum in the header matches the calculated checksum") + + parser.add_argument("rom_path", help="Path to a big endian ROM file", type=Path) + + parser.add_argument("-k", "-c", "--kind", "--cic", help="Used this variant to calculate the checksum instead of automatically detecting which kind the ROM uses", dest="kind", metavar="KIND", choices=ipl3checksum.CICKind.validNames()) + + parser.set_defaults(func=processArguments) diff --git a/src/ipl3checksum/frontends/climain.py b/src/ipl3checksum/frontends/climain.py deleted file mode 100644 index c656b4c..0000000 --- a/src/ipl3checksum/frontends/climain.py +++ /dev/null @@ -1,27 +0,0 @@ -#!/usr/bin/env python3 - -# SPDX-FileCopyrightText: © 2023 Decompollaborate -# SPDX-License-Identifier: MIT - -from __future__ import annotations - -import argparse - -from .. import __version__ - - -def ipl3checksumMain(): - parser = argparse.ArgumentParser(description="Interface to call any of the ipl3checksum's CLI utilities", prog="ipl3checksum") - - parser.add_argument("-V", "--version", action="version", version=f"%(prog)s {__version__}") - - # subparsers = parser.add_subparsers(description="action", help="The CLI utility to run", required=True) - - # ipl3checksum.frontends.utility.addSubparser(subparsers) - - args = parser.parse_args() - # args.func(args) - - -if __name__ == "__main__": - ipl3checksumMain() diff --git a/src/ipl3checksum/frontends/detect_cic.py b/src/ipl3checksum/frontends/detect_cic.py new file mode 100644 index 0000000..7330133 --- /dev/null +++ b/src/ipl3checksum/frontends/detect_cic.py @@ -0,0 +1,37 @@ +#!/usr/bin/env python3 + +# SPDX-FileCopyrightText: © 2023 Decompollaborate +# SPDX-License-Identifier: MIT + +from __future__ import annotations + +import argparse +from pathlib import Path + +import ipl3checksum + +def doDetectCic(romBytes: bytes) -> int: + kind = ipl3checksum.detectCIC(romBytes) + + if kind is None: + print(f"Unable to detect CIC kind") + return 1 + + print(f"Detected kind is '{kind.name}'") + + return 0 + + +def processArguments(args: argparse.Namespace): + romPath: Path = args.rom_path + + romBytes = romPath.read_bytes() + + exit(doDetectCic(romBytes)) + +def addSubparser(subparser: argparse._SubParsersAction[argparse.ArgumentParser]): + parser = subparser.add_parser("detect_cic", help="Detects the CIC variant of a given rom") + + parser.add_argument("rom_path", help="Path to a big endian ROM file", type=Path) + + parser.set_defaults(func=processArguments) diff --git a/src/ipl3checksum/frontends/sum.py b/src/ipl3checksum/frontends/sum.py new file mode 100644 index 0000000..6d4f557 --- /dev/null +++ b/src/ipl3checksum/frontends/sum.py @@ -0,0 +1,69 @@ +#!/usr/bin/env python3 + +# SPDX-FileCopyrightText: © 2023 Decompollaborate +# SPDX-License-Identifier: MIT + +from __future__ import annotations + +import argparse +from pathlib import Path +import struct + +import ipl3checksum + +def doSum(romBytes: bytes, kindName: str | None, update: bool, outputPath: Path) -> int: + if kindName is None: + # Detect kind if none was specified by the user + kind = ipl3checksum.detectCIC(romBytes) + if kind is None: + print(f"Unable to detect CIC kind") + return 1 + print(f"Detected kind is '{kind.name}'") + else: + kind = ipl3checksum.CICKind.fromName(kindName) + if kind is None: + print(f"Invalid choice for cic kind. Valid choices: {ipl3checksum.CICKind.validNames()}") + return 1 + + checksum = ipl3checksum.calculateChecksum(romBytes, kind) + if checksum is None: + print(f"Unable to calculate checksum") + return 1 + + chk0, chk1 = checksum + print(f"Calculated checksum: {chk0:08X} {chk1:08X}") + + if update: + print(f"Writing updated ROM to '{outputPath}'") + outputPath.parent.mkdir(parents=True, exist_ok=True) + with outputPath.open("wb") as f: + f.write(romBytes[:0x10]) + f.write(struct.pack(f">II", chk0, chk1)) + f.write(romBytes[0x18:]) + + return 0 + + +def processArguments(args: argparse.Namespace): + romPath: Path = args.rom_path + kindName: str|None = args.kind + update: bool = args.update + outputPath: Path|None = args.output + + romBytes = romPath.read_bytes() + + if outputPath is None: + outputPath = romPath + + exit(doSum(romBytes, kindName, update, outputPath)) + +def addSubparser(subparser: argparse._SubParsersAction[argparse.ArgumentParser]): + parser = subparser.add_parser("sum", help="Calculates the ipl3 checksum of a big endian ROM by detecting the CIC it uses.") + + parser.add_argument("rom_path", help="Path to a big endian ROM file", type=Path) + + parser.add_argument("-k", "-c", "--kind", "--cic", help="Used this variant to calculate the checksum instead of automatically detecting which kind the ROM uses", dest="kind", metavar="KIND", choices=ipl3checksum.CICKind.validNames()) + parser.add_argument("-u", "--update", help="Updates the ROM with the calculated checksum. This option modified the input rom unless `--output` is used", action="store_true") + parser.add_argument("-o", "--output", help="Path to write the updated ROM. This option is ignored if `--update` is not used", type=Path) + + parser.set_defaults(func=processArguments) 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..ab5c1d1 --- /dev/null +++ b/src/rs/checksum.rs @@ -0,0 +1,416 @@ +/* SPDX-FileCopyrightText: © 2023 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] +} + +/// 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. +/// +/// ## 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, +) -> 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 mut a3 = v0; + let mut t2 = v0; + let mut t3 = v0; + let mut s0 = v0; + 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); + + //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 || 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; + } + + Ok((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 an error will be returned. +/// +/// ## 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_err()); +/// ``` +pub fn calculate_checksum_autodetect(rom_bytes: &[u8]) -> Result<(u32, u32), Ipl3ChecksumError> { + 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<(), super::Ipl3ChecksumError> { + 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 = CICKind::from_name(folder_name.to_str().unwrap()).unwrap(); + 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!("Used CIC Kind: {:?}", kind); + + 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::*; + use std::borrow::Cow; + + /** + * 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. + */ + + #[pyfunction] + pub(crate) fn calculateChecksum( + rom_bytes: Cow<[u8]>, + kind: super::CICKind, + ) -> Result, super::Ipl3ChecksumError> { + match super::calculate_checksum(&rom_bytes, kind) { + Ok(checksum) => Ok(Some(checksum)), + Err(e) => match e { + super::Ipl3ChecksumError::BufferNotBigEnough { + buffer_len: _, + expected_len: _, + } => Ok(None), + _ => Err(e), // To trigger an exception on Python's side + }, + } + } + + #[pyfunction] + pub(crate) fn calculateChecksumAutodetect( + rom_bytes: Cow<[u8]>, + ) -> Result, super::Ipl3ChecksumError> { + match super::calculate_checksum_autodetect(&rom_bytes) { + Ok(checksum) => Ok(Some(checksum)), + Err(e) => match e { + super::Ipl3ChecksumError::BufferNotBigEnough { + buffer_len: _, + expected_len: _, + } => Ok(None), + _ => Err(e), // To trigger an exception on Python's side + }, + } + } +} + +#[cfg(feature = "c_bindings")] +mod c_bindings { + #[no_mangle] + pub extern "C" fn ipl3checksum_calculate_checksum( + dst_checksum0: *mut u32, + dst_checksum1: *mut u32, + rom_bytes_len: usize, + rom_bytes: *const u8, + kind: super::CICKind, + ) -> super::Ipl3ChecksumError { + if dst_checksum0.is_null() || dst_checksum1.is_null() || rom_bytes.is_null() { + return super::Ipl3ChecksumError::NullPointer; + } + + let bytes = + match super::utils::c_bindings::u8_vec_from_pointer_array(rom_bytes_len, rom_bytes) { + Err(e) => return e, + Ok(d) => d, + }; + + let checksum = match super::calculate_checksum(&bytes, kind) { + Ok(chk) => chk, + Err(e) => return e, + }; + + unsafe { *dst_checksum0 = checksum.0 }; + unsafe { *dst_checksum1 = checksum.1 }; + + super::Ipl3ChecksumError::Okay + } + + #[no_mangle] + pub extern "C" fn ipl3checksum_calculate_checksum_autodetect( + dst_checksum0: *mut u32, + dst_checksum1: *mut u32, + rom_bytes_len: usize, + rom_bytes: *const u8, + ) -> super::Ipl3ChecksumError { + if dst_checksum0.is_null() || dst_checksum1.is_null() || rom_bytes.is_null() { + return super::Ipl3ChecksumError::NullPointer; + } + + let bytes = + match super::utils::c_bindings::u8_vec_from_pointer_array(rom_bytes_len, rom_bytes) { + Err(e) => return e, + Ok(d) => d, + }; + + let checksum = match super::calculate_checksum_autodetect(&bytes) { + Ok(chk) => chk, + Err(e) => return e, + }; + + unsafe { *dst_checksum0 = checksum.0 }; + unsafe { *dst_checksum1 = checksum.1 }; + + super::Ipl3ChecksumError::Okay + } +} diff --git a/src/rs/cickinds.rs b/src/rs/cickinds.rs new file mode 100644 index 0000000..b8f4e2b --- /dev/null +++ b/src/rs/cickinds.rs @@ -0,0 +1,325 @@ +/* SPDX-FileCopyrightText: © 2023 Decompollaborate */ +/* SPDX-License-Identifier: MIT */ + +#[cfg(feature = "python_bindings")] +use pyo3::prelude::*; + +use crate::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"))] +// 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 +#[cfg_attr(feature = "c_bindings", repr(C))] +#[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 + + CIC_5101, // Aleck 64 +} + +impl CICKind { + pub fn get_seed(&self) -> u32 { + match self { + Self::CIC_6101 | Self::CIC_6102_7101 | Self::CIC_7102 => 0x3F, + Self::CIC_X103 => 0x78, + Self::CIC_X105 => 0x91, + Self::CIC_X106 => 0x85, + Self::CIC_5101 => 0xAC, // Got by brute force, need to verify + } + } + + pub fn get_magic(&self) -> u32 { + match self { + Self::CIC_6101 | Self::CIC_6102_7101 | Self::CIC_7102 | Self::CIC_X105 => 0x5D588B65, + Self::CIC_X103 | Self::CIC_X106 | Self::CIC_5101 => 0x6C078965, + } + } + + pub fn get_hash_md5(&self) -> &'static str { + match self { + Self::CIC_6101 => "900b4a5b68edb71f4c7ed52acd814fc5", + Self::CIC_6102_7101 => "e24dd796b2fa16511521139d28c8356b", + Self::CIC_7102 => "955894c2e40a698bf98a67b78a4e28fa", + Self::CIC_X103 => "319038097346e12c26c3c21b56f86f23", + Self::CIC_X105 => "ff22a296e55d34ab0a077dc2ba5f5796", + Self::CIC_X106 => "6460387749ac0bd925aa5430bc7864fe", + Self::CIC_5101 => "711f8c3ac54fc70a42626bf6c171443d", + } + } + + pub fn from_hash_md5(hash_str: &str) -> Result { + match hash_str { + "900b4a5b68edb71f4c7ed52acd814fc5" => Ok(Self::CIC_6101), + "e24dd796b2fa16511521139d28c8356b" => Ok(Self::CIC_6102_7101), + "955894c2e40a698bf98a67b78a4e28fa" => Ok(Self::CIC_7102), + "319038097346e12c26c3c21b56f86f23" => Ok(Self::CIC_X103), + "ff22a296e55d34ab0a077dc2ba5f5796" => Ok(Self::CIC_X105), + "6460387749ac0bd925aa5430bc7864fe" => Ok(Self::CIC_X106), + "711f8c3ac54fc70a42626bf6c171443d" => Ok(Self::CIC_5101), + _ => Err(Ipl3ChecksumError::UnableToDetectCIC), + } + } + + pub fn get_name(&self) -> &'static str { + match self { + Self::CIC_6101 => "CIC_6101", + Self::CIC_6102_7101 => "CIC_6102_7101", + Self::CIC_7102 => "CIC_7102", + Self::CIC_X103 => "CIC_X103", + Self::CIC_X105 => "CIC_X105", + Self::CIC_X106 => "CIC_X106", + Self::CIC_5101 => "CIC_5101", + } + } + + pub fn from_name(name: &str) -> Result { + match name { + "CIC_6101" | "6101" => Ok(Self::CIC_6101), + "CIC_6102_7101" | "CIC_6102" | "CIC_7101" | "6102_7101" | "6102" | "7101" => { + Ok(Self::CIC_6102_7101) + } + "CIC_7102" | "7102" => Ok(Self::CIC_7102), + "CIC_X103" | "CIC_6103" | "CIC_7103" | "X103" | "6103" | "7103" => Ok(Self::CIC_X103), + "CIC_X105" | "CIC_6105" | "CIC_7105" | "X105" | "6105" | "7105" => Ok(Self::CIC_X105), + "CIC_X106" | "CIC_6106" | "CIC_7106" | "X106" | "6106" | "7106" => Ok(Self::CIC_X106), + "CIC_5101" | "5101" => Ok(Self::CIC_5101), + _ => Err(Ipl3ChecksumError::UnableToDetectCIC), + } + } + + /// Returns a Vec of valid names for `from_name` + pub fn valid_names() -> Vec<&'static str> { + vec![ + "CIC_6101", + "6101", + "CIC_6102_7101", + "CIC_6102", + "CIC_7101", + "6102_7101", + "6102", + "7101", + "CIC_7102", + "7102", + "CIC_X103", + "CIC_6103", + "CIC_7103", + "X103", + "6103", + "7103", + "CIC_X105", + "CIC_6105", + "CIC_7105", + "X105", + "6105", + "7105", + "CIC_X106", + "CIC_6106", + "CIC_7106", + "X106", + "6106", + "7106", + "CIC_5101", + "5101", + ] + } + + pub fn from_value(value: usize) -> Result { + match value { + 6101 => Ok(Self::CIC_6101), + 6102 | 7101 => Ok(Self::CIC_6102_7101), + 7102 => Ok(Self::CIC_7102), + 6103 | 7103 => Ok(Self::CIC_X103), + 6105 | 7105 => Ok(Self::CIC_X105), + 6106 | 7106 => Ok(Self::CIC_X106), + 5101 => Ok(Self::CIC_5101), + _ => Err(Ipl3ChecksumError::UnableToDetectCIC), + } + } +} + +#[cfg(feature = "python_bindings")] +#[allow(non_snake_case)] +mod python_bindings { + use pyo3::prelude::*; + + use crate::Ipl3ChecksumError; + + #[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) -> Result, Ipl3ChecksumError> { + match Self::from_hash_md5(hash_str) { + Ok(kind) => Ok(Some(kind)), + Err(e) => match e { + Ipl3ChecksumError::UnableToDetectCIC => Ok(None), + _ => Err(e), + }, + } + } + + #[getter] + pub fn name(&self) -> &str { + self.get_name() + } + + #[staticmethod] + pub fn fromName(name: &str) -> Result, Ipl3ChecksumError> { + match Self::from_name(name) { + Ok(kind) => Ok(Some(kind)), + Err(e) => match e { + Ipl3ChecksumError::UnableToDetectCIC => Ok(None), + _ => Err(e), + }, + } + } + + #[staticmethod] + pub fn validNames() -> Vec<&'static str> { + Self::valid_names() + } + + #[staticmethod] + pub fn fromValue(value: usize) -> Result, Ipl3ChecksumError> { + match Self::from_value(value) { + Ok(kind) => Ok(Some(kind)), + Err(e) => match e { + Ipl3ChecksumError::UnableToDetectCIC => Ok(None), + _ => Err(e), + }, + } + } + } +} + +#[cfg(feature = "c_bindings")] +mod c_bindings { + use crate::{utils, CICKind, Ipl3ChecksumError}; + + #[no_mangle] + pub extern "C" fn ipl3checksum_cickind_get_seed(kind: CICKind) -> u32 { + kind.get_seed() + } + + #[no_mangle] + pub extern "C" fn ipl3checksum_cickind_get_magic(kind: CICKind) -> u32 { + kind.get_magic() + } + + #[no_mangle] + pub extern "C" fn ipl3checksum_cickind_get_hash_md5( + kind: CICKind, + dst_hash: *mut *mut core::ffi::c_char, + ) -> Ipl3ChecksumError { + let hash = match utils::c_bindings::c_string_from_rust_str(kind.get_hash_md5()) { + Err(e) => return e, + Ok(s) => s, + }; + + unsafe { *dst_hash = hash }; + + Ipl3ChecksumError::Okay + } + + #[no_mangle] + pub extern "C" fn ipl3checksum_cickind_from_hash_md5( + kind_dst: *mut CICKind, + hash_str: *const core::ffi::c_char, + ) -> Ipl3ChecksumError { + if kind_dst.is_null() || hash_str.is_null() { + return Ipl3ChecksumError::NullPointer; + } + + let hash = match utils::c_bindings::static_str_from_c_string(hash_str) { + Err(e) => return e, + Ok(h) => h, + }; + + let kind = match CICKind::from_hash_md5(hash) { + Ok(k) => k, + Err(e) => return e, + }; + + unsafe { *kind_dst = kind }; + + Ipl3ChecksumError::Okay + } + + #[no_mangle] + pub extern "C" fn ipl3checksum_cickind_get_name( + kind: CICKind, + dst_name: *mut *mut core::ffi::c_char, + ) -> Ipl3ChecksumError { + let name = match utils::c_bindings::c_string_from_rust_str(kind.get_name()) { + Err(e) => return e, + Ok(s) => s, + }; + + unsafe { *dst_name = name }; + + Ipl3ChecksumError::Okay + } + + #[no_mangle] + pub extern "C" fn ipl3checksum_cickind_from_name( + kind_dst: *mut CICKind, + c_name: *const core::ffi::c_char, + ) -> Ipl3ChecksumError { + if kind_dst.is_null() || c_name.is_null() { + return Ipl3ChecksumError::NullPointer; + } + + let name = match utils::c_bindings::static_str_from_c_string(c_name) { + Err(e) => return e, + Ok(h) => h, + }; + + let kind = match CICKind::from_name(name) { + Ok(k) => k, + Err(e) => return e, + }; + + unsafe { *kind_dst = kind }; + + Ipl3ChecksumError::Okay + } + + #[no_mangle] + pub extern "C" fn ipl3checksum_cickind_from_value( + kind_dst: *mut CICKind, + value: usize, + ) -> Ipl3ChecksumError { + if kind_dst.is_null() { + return Ipl3ChecksumError::NullPointer; + } + + let kind = match CICKind::from_value(value) { + Ok(k) => k, + Err(e) => return e, + }; + + unsafe { *kind_dst = kind }; + + Ipl3ChecksumError::Okay + } +} diff --git a/src/rs/detect.rs b/src/rs/detect.rs new file mode 100644 index 0000000..e9345b0 --- /dev/null +++ b/src/rs/detect.rs @@ -0,0 +1,139 @@ +/* SPDX-FileCopyrightText: © 2023 Decompollaborate */ +/* SPDX-License-Identifier: MIT */ + +use crate::{cickinds::CICKind, error::Ipl3ChecksumError, utils}; + +/// Tries to detect an IPL3 binary. +/// +/// The argument to this function must be exactly the IPL3 binary, meaning the +/// binary size must match exactly the one of an IPL3 binary. +/// +/// ## Arguments +/// +/// * `raw_bytes` - IPL3 binary in big endian format. +/// +/// ## Return +/// +/// * The detected CIC kind, or `Ipl3ChecksumError` if was not able to detect the CIC kind. +pub fn detect_cic_raw(raw_bytes: &[u8]) -> Result { + if raw_bytes.len() != 0xFC0 { + return Err(Ipl3ChecksumError::BufferSizeIsWrong { + buffer_len: raw_bytes.len(), + expected_len: 0xFC0, + }); + } + + 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]) -> Result { + 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], + ) -> Result, super::Ipl3ChecksumError> { + match super::detect_cic_raw(raw_bytes) { + Ok(cic) => Ok(Some(cic)), + Err(e) => match e { + super::Ipl3ChecksumError::BufferSizeIsWrong { + buffer_len: _, + expected_len: _, + } => Ok(None), + super::Ipl3ChecksumError::UnableToDetectCIC => Ok(None), + _ => Err(e), // To trigger an exception on Python's side + }, + } + } + + #[pyfunction] + pub(crate) fn detectCIC( + rom_bytes: &[u8], + ) -> Result, super::Ipl3ChecksumError> { + match super::detect_cic(rom_bytes) { + Ok(cic) => Ok(Some(cic)), + Err(e) => match e { + super::Ipl3ChecksumError::BufferSizeIsWrong { + buffer_len: _, + expected_len: _, + } => Ok(None), + super::Ipl3ChecksumError::UnableToDetectCIC => Ok(None), + _ => Err(e), // To trigger an exception on Python's side + }, + } + } +} + +#[cfg(feature = "c_bindings")] +mod c_bindings { + use crate::{utils, CICKind, Ipl3ChecksumError}; + + #[no_mangle] + pub extern "C" fn ipl3checksum_detect_cic_raw( + dst_kind: *mut CICKind, + raw_bytes_len: usize, + raw_bytes: *const u8, + ) -> Ipl3ChecksumError { + if dst_kind.is_null() || raw_bytes.is_null() { + return Ipl3ChecksumError::NullPointer; + } + + let bytes = match utils::c_bindings::u8_vec_from_pointer_array(raw_bytes_len, raw_bytes) { + Err(e) => return e, + Ok(d) => d, + }; + + let kind = match super::detect_cic_raw(&bytes) { + Err(e) => return e, + Ok(k) => k, + }; + + unsafe { *dst_kind = kind }; + + Ipl3ChecksumError::Okay + } + + #[no_mangle] + pub extern "C" fn ipl3checksum_detect_cic( + dst_kind: *mut CICKind, + rom_bytes_len: usize, + rom_bytes: *const u8, + ) -> Ipl3ChecksumError { + if dst_kind.is_null() || rom_bytes.is_null() { + return Ipl3ChecksumError::NullPointer; + } + + let bytes = match utils::c_bindings::u8_vec_from_pointer_array(rom_bytes_len, rom_bytes) { + Err(e) => return e, + Ok(d) => d, + }; + + let kind = match super::detect_cic(&bytes) { + Err(e) => return e, + Ok(k) => k, + }; + + unsafe { *dst_kind = kind }; + + Ipl3ChecksumError::Okay + } +} diff --git a/src/rs/error.rs b/src/rs/error.rs new file mode 100644 index 0000000..f5206bc --- /dev/null +++ b/src/rs/error.rs @@ -0,0 +1,54 @@ +/* SPDX-FileCopyrightText: © 2023 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 +#[cfg_attr(feature = "c_bindings", repr(C))] +#[derive(Clone, Debug, PartialEq, Eq, Hash, thiserror::Error)] +pub enum Ipl3ChecksumError { + #[cfg(feature = "c_bindings")] + #[error("Not an error")] + Okay, + #[cfg(feature = "c_bindings")] + #[error("Pointer is null")] + NullPointer, + #[cfg(feature = "c_bindings")] + #[error("Failed to convert a FFI string")] + StringConversion, + + #[error("Unaligned read at offset 0x{offset:X}")] + UnalignedRead { offset: usize }, + #[error("Failed to convert bytes at offset 0x{offset:X}")] + 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}")] + OutOfBounds { + offset: usize, + requested_bytes: usize, + buffer_len: usize, + }, + #[error("The input byte buffer is not big enough. It should be at least 0x{expected_len:X} bytes long, but it was 0x{buffer_len:X} bytes")] + BufferNotBigEnough { + buffer_len: usize, + expected_len: usize, + }, + #[error("The input byte buffer didn't have the expected size. It should be exactly 0x{expected_len:X} bytes long, but it was 0x{buffer_len:X} bytes")] + BufferSizeIsWrong { + buffer_len: usize, + expected_len: usize, + }, + #[error("Unable to detect CIC variant")] + UnableToDetectCIC, +} + +#[cfg(feature = "python_bindings")] +impl std::convert::From for PyErr { + fn from(err: Ipl3ChecksumError) -> PyErr { + PyRuntimeError::new_err(err.to_string()) + } +} diff --git a/src/rs/lib.rs b/src/rs/lib.rs new file mode 100644 index 0000000..eda37fd --- /dev/null +++ b/src/rs/lib.rs @@ -0,0 +1,37 @@ +/* SPDX-FileCopyrightText: © 2023 Decompollaborate */ +/* SPDX-License-Identifier: MIT */ + +mod checksum; +mod cickinds; +mod detect; +mod error; +mod utils; +pub mod version; + +pub use checksum::*; +pub use cickinds::*; +pub use detect::*; +pub use error::*; + +#[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..707a3a0 --- /dev/null +++ b/src/rs/utils.rs @@ -0,0 +1,105 @@ +/* SPDX-FileCopyrightText: © 2023 Decompollaborate */ +/* SPDX-License-Identifier: MIT */ + +use crate::error::Ipl3ChecksumError; + +pub(crate) fn read_u32(bytes: &[u8], offset: usize) -> Result { + if offset % 4 != 0 { + return Err(Ipl3ChecksumError::UnalignedRead { offset }); + } + + if offset + 4 > bytes.len() { + return Err(Ipl3ChecksumError::OutOfBounds { + offset, + requested_bytes: 4, + buffer_len: bytes.len(), + }); + } + + match bytes[offset..offset + 4].try_into() { + Ok(bytes) => Ok(u32::from_be_bytes(bytes)), + Err(_error) => Err(Ipl3ChecksumError::ByteConversion { offset }), + } +} + +pub(crate) fn read_u32_vec( + bytes: &[u8], + offset: usize, + len: usize, +) -> Result, Ipl3ChecksumError> { + let mut ret = Vec::with_capacity(len); + + for i in 0..len { + ret.push(read_u32(bytes, offset + i * 4).unwrap()); + } + + Ok(ret) +} + +pub(crate) fn get_hash_md5(bytes: &[u8]) -> String { + format!("{:x}", md5::compute(bytes)) +} + +#[cfg(feature = "c_bindings")] +pub(crate) mod c_bindings { + use crate::Ipl3ChecksumError; + + pub(crate) fn u8_vec_from_pointer_array( + src_len: usize, + src: *const u8, + ) -> Result, Ipl3ChecksumError> { + if src.is_null() { + return Err(Ipl3ChecksumError::NullPointer); + } + + let mut bytes = Vec::with_capacity(src_len); + + for i in 0..src_len { + bytes.push(unsafe { *src.add(i) }); + } + + Ok(bytes) + } + + pub(crate) fn static_str_from_c_string( + c_str: *const core::ffi::c_char, + ) -> Result<&'static str, Ipl3ChecksumError> { + let converted = unsafe { std::ffi::CStr::from_ptr(c_str) }; + + match converted.to_str() { + Err(_) => Err(Ipl3ChecksumError::StringConversion), + Ok(s) => Ok(s), + } + } + + pub(crate) fn free_c_string(s: *mut core::ffi::c_char) -> Result<(), Ipl3ChecksumError> { + if s.is_null() { + return Err(Ipl3ChecksumError::NullPointer); + } + + unsafe { + drop(std::ffi::CString::from_raw(s)); + } + + Ok(()) + } + + #[no_mangle] + pub extern "C" fn ipl3checksum_free_string(s: *mut core::ffi::c_char) -> Ipl3ChecksumError { + match free_c_string(s) { + Err(e) => e, + Ok(_) => Ipl3ChecksumError::Okay, + } + } + + pub(crate) fn c_string_from_rust_str( + s: &str, + ) -> Result<*mut core::ffi::c_char, Ipl3ChecksumError> { + let c_str_song = match std::ffi::CString::new(s) { + Err(_) => return Err(Ipl3ChecksumError::StringConversion), + Ok(c_s) => c_s, + }; + + Ok(c_str_song.into_raw()) + } +} diff --git a/src/rs/version.rs b/src/rs/version.rs new file mode 100644 index 0000000..01625be --- /dev/null +++ b/src/rs/version.rs @@ -0,0 +1,32 @@ +/* SPDX-FileCopyrightText: © 2023 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 = 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.0"; + +pub static AUTHOR: &str = "Decompollaborate"; + +#[cfg(feature = "c_bindings")] +mod c_bindings { + #[no_mangle] + static ipl3checksum_version_major: i32 = super::VERSION_MAJOR; + #[no_mangle] + static ipl3checksum_version_minor: i32 = super::VERSION_MINOR; + #[no_mangle] + static ipl3checksum_version_patch: i32 = super::VERSION_PATCH; + + // TODO: construct this from super::VERSION_STR + #[no_mangle] + static ipl3checksum_version_str: &[u8] = b"1.1.0\0"; + + // TODO: construct this from super::AUTHOR + #[no_mangle] + static ipl3checksum_version_author: &[u8] = b"Decompollaborate\0"; +} diff --git a/tests/check_correct_sum.py b/tests/check_correct_sum.py index 609d35b..183bdb7 100755 --- a/tests/check_correct_sum.py +++ b/tests/check_correct_sum.py @@ -9,21 +9,13 @@ from pathlib import Path import struct -cicsmapping = { - "CIC_6101": ipl3checksum.CICKind.CIC_6101, - "CIC_6102_7101": ipl3checksum.CICKind.CIC_6102_7101, - "CIC_7102": ipl3checksum.CICKind.CIC_7102, - "CIC_X103": ipl3checksum.CICKind.CIC_X103, - "CIC_X105": ipl3checksum.CICKind.CIC_X105, - "CIC_X106": ipl3checksum.CICKind.CIC_X106, -} - print(f"Running ipl3checksum version {ipl3checksum.__version__}") for ipl3folder in sorted(Path("tests/dummytests").iterdir()): print(ipl3folder.name) - kind = cicsmapping[ipl3folder.name] + kind = ipl3checksum.CICKind.fromName(ipl3folder.name) + assert kind is not None for binPath in sorted(ipl3folder.iterdir()): print(binPath) 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] diff --git a/tests/dummytests/CIC_5101/dummy.bin b/tests/dummytests/CIC_5101/dummy.bin new file mode 100644 index 0000000..aac31b5 Binary files /dev/null and b/tests/dummytests/CIC_5101/dummy.bin differ diff --git a/tests/gen_dummy_bin.py b/tests/gen_dummy_bin.py index 30ce33b..eb05572 100755 --- a/tests/gen_dummy_bin.py +++ b/tests/gen_dummy_bin.py @@ -18,6 +18,7 @@ (ipl3checksum.CICKind.CIC_X103, ), (ipl3checksum.CICKind.CIC_X105, ), (ipl3checksum.CICKind.CIC_X106, ), + (ipl3checksum.CICKind.CIC_5101, ), ] for kind, in cics: