From 636cbea59c364b37ae79a80c57d742f1eb9f040e Mon Sep 17 00:00:00 2001 From: Luke Street Date: Sun, 27 Nov 2022 01:37:29 -0500 Subject: [PATCH] Initial commit --- .cargo/config.toml | 5 + .github/workflows/build.yml | 132 ++++++++ .gitignore | 2 + Cargo.lock | 659 ++++++++++++++++++++++++++++++++++++ Cargo.toml | 38 +++ LICENSE-APACHE | 201 +++++++++++ LICENSE-MIT | 21 ++ README.md | 58 ++++ build.rs | 4 + deny.toml | 205 +++++++++++ rustfmt.toml | 8 + src/argh_version.rs | 65 ++++ src/cmd/demangle.rs | 26 ++ src/cmd/elf2dol.rs | 173 ++++++++++ src/cmd/map.rs | 140 ++++++++ src/cmd/metroidbuildinfo.rs | 47 +++ src/cmd/mod.rs | 5 + src/cmd/shasum.rs | 88 +++++ src/main.rs | 39 +++ src/util/map.rs | 485 ++++++++++++++++++++++++++ src/util/mod.rs | 1 + 21 files changed, 2402 insertions(+) create mode 100644 .cargo/config.toml create mode 100644 .github/workflows/build.yml create mode 100644 .gitignore create mode 100644 Cargo.lock create mode 100644 Cargo.toml create mode 100644 LICENSE-APACHE create mode 100644 LICENSE-MIT create mode 100644 README.md create mode 100644 build.rs create mode 100644 deny.toml create mode 100644 rustfmt.toml create mode 100644 src/argh_version.rs create mode 100644 src/cmd/demangle.rs create mode 100644 src/cmd/elf2dol.rs create mode 100644 src/cmd/map.rs create mode 100644 src/cmd/metroidbuildinfo.rs create mode 100644 src/cmd/mod.rs create mode 100644 src/cmd/shasum.rs create mode 100644 src/main.rs create mode 100644 src/util/map.rs create mode 100644 src/util/mod.rs diff --git a/.cargo/config.toml b/.cargo/config.toml new file mode 100644 index 0000000..beb596d --- /dev/null +++ b/.cargo/config.toml @@ -0,0 +1,5 @@ +[target.aarch64-unknown-linux-gnu] +linker = "aarch64-linux-gnu-gcc" + +[target.armv7-unknown-linux-gnueabihf] +linker = "arm-linux-gnueabihf-gcc" diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml new file mode 100644 index 0000000..f271f20 --- /dev/null +++ b/.github/workflows/build.yml @@ -0,0 +1,132 @@ +name: Build + +on: [ push, pull_request ] + +env: + CARGO_BIN_NAME: dtk + CARGO_TARGET_DIR: target + +jobs: + check: + name: Check + runs-on: ubuntu-latest + env: + RUSTFLAGS: -D warnings + steps: + - name: Checkout + uses: actions/checkout@v3 + - name: Setup Rust toolchain + uses: dtolnay/rust-toolchain@stable + with: + components: rustfmt, clippy + - name: Cargo check + run: cargo check --all-features + - name: Cargo clippy + run: cargo clippy --all-features + + deny: + name: Deny + runs-on: ubuntu-latest + strategy: + matrix: + checks: + - advisories + - bans licenses sources + # Prevent new advisories from failing CI + continue-on-error: ${{ matrix.checks == 'advisories' }} + steps: + - uses: actions/checkout@v3 + - uses: EmbarkStudios/cargo-deny-action@v1 + with: + command: check ${{ matrix.checks }} + + test: + name: Test + strategy: + matrix: + platform: [ ubuntu-latest, windows-latest, macos-latest ] + fail-fast: false + runs-on: ${{ matrix.platform }} + steps: + - name: Checkout + uses: actions/checkout@v3 + - name: Setup Rust toolchain + uses: dtolnay/rust-toolchain@stable + - name: Cargo test + run: cargo test --release --all-features + + build: + name: Build + strategy: + matrix: + include: + - platform: ubuntu-latest + target: x86_64-unknown-linux-gnu + name: linux-x86_64 + - platform: ubuntu-latest + target: aarch64-unknown-linux-gnu + name: linux-aarch64 + packages: gcc-aarch64-linux-gnu + - platform: ubuntu-latest + target: armv7-unknown-linux-gnueabihf + name: linux-armv7l + packages: gcc-arm-linux-gnueabihf + - platform: windows-latest + target: x86_64-pc-windows-msvc + name: windows-x86_64 + - platform: windows-latest + target: aarch64-pc-windows-msvc + name: windows-arm64 + - platform: macos-latest + target: x86_64-apple-darwin + name: macos-x86_64 + - platform: macos-latest + target: aarch64-apple-darwin + name: macos-arm64 + fail-fast: false + runs-on: ${{ matrix.platform }} + steps: + - name: Checkout + uses: actions/checkout@v3 + - name: Install dependencies + if: matrix.packages != '' + run: sudo apt-get -y install ${{ matrix.packages }} + - name: Setup Rust toolchain + uses: dtolnay/rust-toolchain@stable + with: + targets: ${{ matrix.target }} + - name: Cargo build + run: cargo build --release --all-features --target ${{ matrix.target }} --bin ${{ env.CARGO_BIN_NAME }} + - name: Upload artifacts + uses: actions/upload-artifact@v3 + with: + name: ${{ matrix.name }} + path: | + ${{ env.CARGO_TARGET_DIR }}/release/${{ env.CARGO_BIN_NAME }} + ${{ env.CARGO_TARGET_DIR }}/release/${{ env.CARGO_BIN_NAME }}.exe + ${{ env.CARGO_TARGET_DIR }}/${{ matrix.target }}/release/${{ env.CARGO_BIN_NAME }} + ${{ env.CARGO_TARGET_DIR }}/${{ matrix.target }}/release/${{ env.CARGO_BIN_NAME }}.exe + if-no-files-found: error + + release: + name: Release + if: startsWith(github.ref, 'refs/tags/') + runs-on: ubuntu-latest + needs: [ build ] + steps: + - name: Download artifacts + uses: actions/download-artifact@v3 + with: + path: artifacts + - name: Rename artifacts + working-directory: artifacts + run: | + mkdir ../out + for i in */*/release/$CARGO_BIN_NAME*; do + mv "$i" "../out/$(sed -E "s/([^/]+)\/[^/]+\/release\/($CARGO_BIN_NAME)/\2-\1/" <<< "$i")" + done + ls -R ../out + - name: Release + uses: softprops/action-gh-release@v1 + with: + files: out/* diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..c1ee0b9 --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +/build +/.idea diff --git a/Cargo.lock b/Cargo.lock new file mode 100644 index 0000000..7af6e72 --- /dev/null +++ b/Cargo.lock @@ -0,0 +1,659 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 3 + +[[package]] +name = "aho-corasick" +version = "0.7.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cc936419f96fa211c1b9166887b38e5e40b19958e5b895be7c1f93adec7071ac" +dependencies = [ + "memchr", +] + +[[package]] +name = "anyhow" +version = "1.0.66" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "216261ddc8289130e551ddcd5ce8a064710c0d064a4d2895c67151c92b5443f6" + +[[package]] +name = "argh" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c375edecfd2074d5edcc31396860b6e54b6f928714d0e097b983053fac0cabe3" +dependencies = [ + "argh_derive", + "argh_shared", +] + +[[package]] +name = "argh_derive" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aa013479b80109a1bf01a039412b0f0013d716f36921226d86c6709032fb7a03" +dependencies = [ + "argh_shared", + "heck", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "argh_shared" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "149f75bbec1827618262e0855a68f0f9a7f2edc13faebf33c4f16d6725edb6a9" + +[[package]] +name = "atty" +version = "0.2.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9b39be18770d11421cdb1b9947a45dd3f37e93092cbf377614828a319d5fee8" +dependencies = [ + "hermit-abi", + "libc", + "winapi", +] + +[[package]] +name = "base16ct" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "349a06037c7bf932dd7e7d1f653678b2038b9ad46a74102f1fc7bd7872678cce" + +[[package]] +name = "bitflags" +version = "1.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" + +[[package]] +name = "block-buffer" +version = "0.10.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "69cce20737498f97b993470a6e536b8523f0af7892a4f928cceb1ac5e52ebe7e" +dependencies = [ + "generic-array", +] + +[[package]] +name = "cc" +version = "1.0.77" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e9f73505338f7d905b19d18738976aae232eb46b8efc15554ffc56deb5d9ebe4" +dependencies = [ + "jobserver", +] + +[[package]] +name = "cfg-if" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" + +[[package]] +name = "cpufeatures" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "28d997bd5e24a5928dd43e46dc529867e207907fe0b239c3477d924f7f2ca320" +dependencies = [ + "libc", +] + +[[package]] +name = "crypto-common" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3" +dependencies = [ + "generic-array", + "typenum", +] + +[[package]] +name = "cwdemangle" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e411efa4ed072fa5bdb637c945ea7f618ebd416748cecc255b00968c1db81e68" +dependencies = [ + "argh", +] + +[[package]] +name = "decomp-toolkit" +version = "0.1.0" +dependencies = [ + "anyhow", + "argh", + "base16ct", + "cwdemangle", + "hex", + "lazy_static", + "log", + "memchr", + "memmap2", + "multimap", + "object", + "pretty_env_logger", + "regex", + "sha-1", + "topological-sort", + "vergen", +] + +[[package]] +name = "digest" +version = "0.10.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8168378f4e5023e7218c89c891c0fd8ecdb5e5e4f18cb78f38cf245dd021e76f" +dependencies = [ + "block-buffer", + "crypto-common", +] + +[[package]] +name = "enum-iterator" +version = "1.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "45a0ac4aeb3a18f92eaf09c6bb9b3ac30ff61ca95514fc58cbead1c9a6bf5401" +dependencies = [ + "enum-iterator-derive", +] + +[[package]] +name = "enum-iterator-derive" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "828de45d0ca18782232dfb8f3ea9cc428e8ced380eb26a520baaacfc70de39ce" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "env_logger" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "44533bbbb3bb3c1fa17d9f2e4e38bbbaf8396ba82193c4cb1b6445d711445d36" +dependencies = [ + "atty", + "humantime", + "log", + "regex", + "termcolor", +] + +[[package]] +name = "form_urlencoded" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a9c384f161156f5260c24a097c56119f9be8c798586aecc13afbcbe7b7e26bf8" +dependencies = [ + "percent-encoding", +] + +[[package]] +name = "generic-array" +version = "0.14.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bff49e947297f3312447abdca79f45f4738097cc82b06e72054d2223f601f1b9" +dependencies = [ + "typenum", + "version_check", +] + +[[package]] +name = "getset" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e45727250e75cc04ff2846a66397da8ef2b3db8e40e0cef4df67950a07621eb9" +dependencies = [ + "proc-macro-error", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "git2" +version = "0.14.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d0155506aab710a86160ddb504a480d2964d7ab5b9e62419be69e0032bc5931c" +dependencies = [ + "bitflags", + "libc", + "libgit2-sys", + "log", + "url", +] + +[[package]] +name = "heck" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2540771e65fc8cb83cd6e8a237f70c319bd5c29f78ed1084ba5d50eeac86f7f9" + +[[package]] +name = "hermit-abi" +version = "0.1.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "62b467343b94ba476dcb2500d242dadbb39557df889310ac77c5d99100aaac33" +dependencies = [ + "libc", +] + +[[package]] +name = "hex" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" + +[[package]] +name = "humantime" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df004cfca50ef23c36850aaaa59ad52cc70d0e90243c3c7737a4dd32dc7a3c4f" +dependencies = [ + "quick-error", +] + +[[package]] +name = "idna" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e14ddfc70884202db2244c223200c204c2bda1bc6e0998d11b5e024d657209e6" +dependencies = [ + "unicode-bidi", + "unicode-normalization", +] + +[[package]] +name = "itoa" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4217ad341ebadf8d8e724e264f13e593e0648f5b3e94b3896a5df283be015ecc" + +[[package]] +name = "jobserver" +version = "0.1.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "068b1ee6743e4d11fb9c6a1e6064b3693a1b600e7f5f5988047d98b3dc9fb90b" +dependencies = [ + "libc", +] + +[[package]] +name = "lazy_static" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" + +[[package]] +name = "libc" +version = "0.2.137" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc7fcc620a3bff7cdd7a365be3376c97191aeaccc2a603e600951e452615bf89" + +[[package]] +name = "libgit2-sys" +version = "0.13.4+1.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d0fa6563431ede25f5cc7f6d803c6afbc1c5d3ad3d4925d12c882bf2b526f5d1" +dependencies = [ + "cc", + "libc", + "libz-sys", + "pkg-config", +] + +[[package]] +name = "libz-sys" +version = "1.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9702761c3935f8cc2f101793272e202c72b99da8f4224a19ddcf1279a6450bbf" +dependencies = [ + "cc", + "libc", + "pkg-config", + "vcpkg", +] + +[[package]] +name = "log" +version = "0.4.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "abb12e687cfb44aa40f41fc3978ef76448f9b6038cad6aef4259d3c095a2382e" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "memchr" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2dffe52ecf27772e601905b7522cb4ef790d2cc203488bbd0e2fe85fcb74566d" + +[[package]] +name = "memmap2" +version = "0.5.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4b182332558b18d807c4ce1ca8ca983b34c3ee32765e47b3f0f69b90355cc1dc" +dependencies = [ + "libc", +] + +[[package]] +name = "multimap" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e5ce46fe64a9d73be07dcbe690a38ce1b293be448fd8ce1e6c1b8062c9f72c6a" +dependencies = [ + "serde", +] + +[[package]] +name = "object" +version = "0.30.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "239da7f290cfa979f43f85a8efeee9a8a76d0827c356d37f9d3d7254d6b537fb" +dependencies = [ + "memchr", +] + +[[package]] +name = "percent-encoding" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "478c572c3d73181ff3c2539045f6eb99e5491218eae919370993b890cdbdd98e" + +[[package]] +name = "pkg-config" +version = "0.3.26" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ac9a59f73473f1b8d852421e59e64809f025994837ef743615c6d0c5b305160" + +[[package]] +name = "pretty_env_logger" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "926d36b9553851b8b0005f1275891b392ee4d2d833852c417ed025477350fb9d" +dependencies = [ + "env_logger", + "log", +] + +[[package]] +name = "proc-macro-error" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da25490ff9892aab3fcf7c36f08cfb902dd3e71ca0f9f9517bea02a73a5ce38c" +dependencies = [ + "proc-macro-error-attr", + "proc-macro2", + "quote", + "syn", + "version_check", +] + +[[package]] +name = "proc-macro-error-attr" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1be40180e52ecc98ad80b184934baf3d0d29f979574e439af5a55274b35f869" +dependencies = [ + "proc-macro2", + "quote", + "version_check", +] + +[[package]] +name = "proc-macro2" +version = "1.0.47" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5ea3d908b0e36316caf9e9e2c4625cdde190a7e6f440d794667ed17a1855e725" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quick-error" +version = "1.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1d01941d82fa2ab50be1e79e6714289dd7cde78eba4c074bc5a4374f650dfe0" + +[[package]] +name = "quote" +version = "1.0.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbe448f377a7d6961e30f5955f9b8d106c3f5e449d493ee1b125c1d43c2b5179" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "regex" +version = "1.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e076559ef8e241f2ae3479e36f97bd5741c0330689e217ad51ce2c76808b868a" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax", +] + +[[package]] +name = "regex-syntax" +version = "0.6.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "456c603be3e8d448b072f410900c09faf164fbce2d480456f50eea6e25f9c848" + +[[package]] +name = "rustversion" +version = "1.0.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97477e48b4cf8603ad5f7aaf897467cf42ab4218a38ef76fb14c2d6773a6d6a8" + +[[package]] +name = "serde" +version = "1.0.147" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d193d69bae983fc11a79df82342761dfbf28a99fc8d203dca4c3c1b590948965" + +[[package]] +name = "sha-1" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "028f48d513f9678cda28f6e4064755b3fbb2af6acd672f2c209b62323f7aea0f" +dependencies = [ + "cfg-if", + "cpufeatures", + "digest", +] + +[[package]] +name = "syn" +version = "1.0.103" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a864042229133ada95abf3b54fdc62ef5ccabe9515b64717bcb9a1919e59445d" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "termcolor" +version = "1.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bab24d30b911b2376f3a13cc2cd443142f0c81dda04c118693e35b3835757755" +dependencies = [ + "winapi-util", +] + +[[package]] +name = "thiserror" +version = "1.0.37" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "10deb33631e3c9018b9baf9dcbbc4f737320d2b576bac10f6aefa048fa407e3e" +dependencies = [ + "thiserror-impl", +] + +[[package]] +name = "thiserror-impl" +version = "1.0.37" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "982d17546b47146b28f7c22e3d08465f6b8903d0ea13c1660d9d84a6e7adcdbb" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "time" +version = "0.3.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a561bf4617eebd33bca6434b988f39ed798e527f51a1e797d0ee4f61c0a38376" +dependencies = [ + "itoa", + "serde", + "time-core", + "time-macros", +] + +[[package]] +name = "time-core" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2e153e1f1acaef8acc537e68b44906d2db6436e2b35ac2c6b42640fff91f00fd" + +[[package]] +name = "time-macros" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d967f99f534ca7e495c575c62638eebc2898a8c84c119b89e250477bc4ba16b2" +dependencies = [ + "time-core", +] + +[[package]] +name = "tinyvec" +version = "1.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "87cc5ceb3875bb20c2890005a4e226a4651264a5c75edb2421b52861a0a0cb50" +dependencies = [ + "tinyvec_macros", +] + +[[package]] +name = "tinyvec_macros" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cda74da7e1a664f795bb1f8a87ec406fb89a02522cf6e50620d016add6dbbf5c" + +[[package]] +name = "topological-sort" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ea68304e134ecd095ac6c3574494fc62b909f416c4fca77e440530221e549d3d" + +[[package]] +name = "typenum" +version = "1.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dcf81ac59edc17cc8697ff311e8f5ef2d99fcbd9817b34cec66f90b6c3dfd987" + +[[package]] +name = "unicode-bidi" +version = "0.3.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "099b7128301d285f79ddd55b9a83d5e6b9e97c92e0ea0daebee7263e932de992" + +[[package]] +name = "unicode-ident" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ceab39d59e4c9499d4e5a8ee0e2735b891bb7308ac83dfb4e80cad195c9f6f3" + +[[package]] +name = "unicode-normalization" +version = "0.1.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c5713f0fc4b5db668a2ac63cdb7bb4469d8c9fed047b1d0292cc7b0ce2ba921" +dependencies = [ + "tinyvec", +] + +[[package]] +name = "url" +version = "2.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0d68c799ae75762b8c3fe375feb6600ef5602c883c5d21eb51c09f22b83c4643" +dependencies = [ + "form_urlencoded", + "idna", + "percent-encoding", +] + +[[package]] +name = "vcpkg" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" + +[[package]] +name = "vergen" +version = "7.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "73ba753d713ec3844652ad2cb7eb56bc71e34213a14faddac7852a10ba88f61e" +dependencies = [ + "anyhow", + "cfg-if", + "enum-iterator", + "getset", + "git2", + "rustversion", + "thiserror", + "time", +] + +[[package]] +name = "version_check" +version = "0.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f" + +[[package]] +name = "winapi" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" +dependencies = [ + "winapi-i686-pc-windows-gnu", + "winapi-x86_64-pc-windows-gnu", +] + +[[package]] +name = "winapi-i686-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" + +[[package]] +name = "winapi-util" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "70ec6ce85bb158151cae5e5c87f95a8e97d2c0c4b001223f33a334e3ce5de178" +dependencies = [ + "winapi", +] + +[[package]] +name = "winapi-x86_64-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..ec136a1 --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,38 @@ +[package] +name = "decomp-toolkit" +description = "GameCube/Wii decompilation project tools." +authors = ["Luke Street "] +license = "MIT OR Apache-2.0" +version = "0.1.0" +edition = "2021" +publish = false +build = "build.rs" +repository = "https://github.com/encounter/decomp-toolkit" +readme = "README.md" +categories = ["command-line-utilities"] + +[[bin]] +name = "dtk" +path = "src/main.rs" + +[dependencies] +anyhow = "1.0.64" +argh = "0.1.8" +base16ct = "0.1.1" +cwdemangle = "0.1.3" +hex = "0.4.3" +lazy_static = "1.4.0" +log = "0.4.17" +memchr = "2.5.0" +memmap2 = "0.5.7" +multimap = "0.8.3" +object = { version = "0.30.0", features = ["read_core", "std", "elf"], default-features = false } +pretty_env_logger = "0.4.0" +regex = "1.6.0" +sha-1 = "0.10.0" +topological-sort = "0.2.2" + +[build-dependencies] +anyhow = "1.0.64" +vergen = { version = "7.4.2", features = ["build", "git"], default-features = false } + diff --git a/LICENSE-APACHE b/LICENSE-APACHE new file mode 100644 index 0000000..e9d5437 --- /dev/null +++ b/LICENSE-APACHE @@ -0,0 +1,201 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "{}" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright 2021 Luke Street. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/LICENSE-MIT b/LICENSE-MIT new file mode 100644 index 0000000..5bf95a4 --- /dev/null +++ b/LICENSE-MIT @@ -0,0 +1,21 @@ +MIT License + +Copyright 2021 Luke Street. + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/README.md b/README.md new file mode 100644 index 0000000..e23aee7 --- /dev/null +++ b/README.md @@ -0,0 +1,58 @@ +# decomp-toolkit [![Build Status]][actions] + +[Build Status]: https://github.com/encounter/decomp-toolkit/actions/workflows/build.yaml/badge.svg +[actions]: https://github.com/encounter/decomp-toolkit/actions + +GameCube/Wii decompilation project tools. + +This provides various commands that assist with creating a build system that works +across all major platforms without dealing with platform-specific C compilers, +UNIX compatibility layers like msys2, or other idiosyncrasies. + +## Commands + +### demangle + +Demangles CodeWarrior C++ symbols. A thin wrapper for [cwdemangle](https://github.com/encounter/cwdemangle). + +```shell +$ dtk demangle 'BuildLight__9CGuiLightCFv' +CGuiLight::BuildLight() const +``` + +### elf2dol + +Creates a DOL file from the provided ELF file. + +```shell +$ dtk elf2dol input.elf output.dol +``` + +### map + +Processes CodeWarrior map files and provides information about symbols and TUs. + +```shell +$ dtk map entries Game.MAP 'Unit.o' +# Outputs all symbols that are referenced by Unit.o +# This is useful for finding deduplicated weak functions, +# which only show on first use in the link map. + +$ dtk map symbol Game.MAP 'Function__5ClassFv' +# Outputs reference information for Function__5ClassFv +# CodeWarrior link maps can get very deeply nested, +# so this is useful for emitting direct references +# in a readable format. +``` + +### shasum + +Calculate and verify SHA-1 hashes. + +```shell +$ dtk shasum baserom.dol +949c5ed7368aef547e0b0db1c3678f466e2afbff baserom.dol + +$ dtk shasum -c baserom.sha1 +baserom.dol: OK +``` diff --git a/build.rs b/build.rs new file mode 100644 index 0000000..e10776e --- /dev/null +++ b/build.rs @@ -0,0 +1,4 @@ +use anyhow::Result; +use vergen::{vergen, Config}; + +fn main() -> Result<()> { vergen(Config::default()) } diff --git a/deny.toml b/deny.toml new file mode 100644 index 0000000..d40c1a0 --- /dev/null +++ b/deny.toml @@ -0,0 +1,205 @@ +# This template contains all of the possible sections and their default values + +# Note that all fields that take a lint level have these possible values: +# * deny - An error will be produced and the check will fail +# * warn - A warning will be produced, but the check will not fail +# * allow - No warning or error will be produced, though in some cases a note +# will be + +# The values provided in this template are the default values that will be used +# when any section or field is not specified in your own configuration + +# If 1 or more target triples (and optionally, target_features) are specified, +# only the specified targets will be checked when running `cargo deny check`. +# This means, if a particular package is only ever used as a target specific +# dependency, such as, for example, the `nix` crate only being used via the +# `target_family = "unix"` configuration, that only having windows targets in +# this list would mean the nix crate, as well as any of its exclusive +# dependencies not shared by any other crates, would be ignored, as the target +# list here is effectively saying which targets you are building for. +targets = [ + # The triple can be any string, but only the target triples built in to + # rustc (as of 1.40) can be checked against actual config expressions + #{ triple = "x86_64-unknown-linux-musl" }, + # You can also specify which target_features you promise are enabled for a + # particular target. target_features are currently not validated against + # the actual valid features supported by the target architecture. + #{ triple = "wasm32-unknown-unknown", features = ["atomics"] }, +] + +# This section is considered when running `cargo deny check advisories` +# More documentation for the advisories section can be found here: +# https://embarkstudios.github.io/cargo-deny/checks/advisories/cfg.html +[advisories] +# The path where the advisory database is cloned/fetched into +db-path = "~/.cargo/advisory-db" +# The url(s) of the advisory databases to use +db-urls = ["https://github.com/rustsec/advisory-db"] +# The lint level for security vulnerabilities +vulnerability = "deny" +# The lint level for unmaintained crates +unmaintained = "warn" +# The lint level for crates that have been yanked from their source registry +yanked = "warn" +# The lint level for crates with security notices. Note that as of +# 2019-12-17 there are no security notice advisories in +# https://github.com/rustsec/advisory-db +notice = "warn" +# A list of advisory IDs to ignore. Note that ignored advisories will still +# output a note when they are encountered. +ignore = [ + #"RUSTSEC-0000-0000", +] +# Threshold for security vulnerabilities, any vulnerability with a CVSS score +# lower than the range specified will be ignored. Note that ignored advisories +# will still output a note when they are encountered. +# * None - CVSS Score 0.0 +# * Low - CVSS Score 0.1 - 3.9 +# * Medium - CVSS Score 4.0 - 6.9 +# * High - CVSS Score 7.0 - 8.9 +# * Critical - CVSS Score 9.0 - 10.0 +#severity-threshold = + +# This section is considered when running `cargo deny check licenses` +# More documentation for the licenses section can be found here: +# https://embarkstudios.github.io/cargo-deny/checks/licenses/cfg.html +[licenses] +# The lint level for crates which do not have a detectable license +unlicensed = "deny" +# List of explictly allowed licenses +# See https://spdx.org/licenses/ for list of possible licenses +# [possible values: any SPDX 3.11 short identifier (+ optional exception)]. +allow = [ + "MIT", + "Apache-2.0", + "BSD-3-Clause", + "Unicode-DFS-2016", + "0BSD", +] +# List of explictly disallowed licenses +# See https://spdx.org/licenses/ for list of possible licenses +# [possible values: any SPDX 3.11 short identifier (+ optional exception)]. +deny = [ + #"Nokia", +] +# Lint level for licenses considered copyleft +copyleft = "warn" +# Blanket approval or denial for OSI-approved or FSF Free/Libre licenses +# * both - The license will be approved if it is both OSI-approved *AND* FSF +# * either - The license will be approved if it is either OSI-approved *OR* FSF +# * osi-only - The license will be approved if is OSI-approved *AND NOT* FSF +# * fsf-only - The license will be approved if is FSF *AND NOT* OSI-approved +# * neither - This predicate is ignored and the default lint level is used +allow-osi-fsf-free = "neither" +# Lint level used when no other predicates are matched +# 1. License isn't in the allow or deny lists +# 2. License isn't copyleft +# 3. License isn't OSI/FSF, or allow-osi-fsf-free = "neither" +default = "deny" +# The confidence threshold for detecting a license from license text. +# The higher the value, the more closely the license text must be to the +# canonical license text of a valid SPDX license file. +# [possible values: any between 0.0 and 1.0]. +confidence-threshold = 0.8 +# Allow 1 or more licenses on a per-crate basis, so that particular licenses +# aren't accepted for every possible crate as with the normal allow list +exceptions = [ + # Each entry is the crate and version constraint, and its specific allow + # list + #{ allow = ["Zlib"], name = "adler32", version = "*" }, +] + +# Some crates don't have (easily) machine readable licensing information, +# adding a clarification entry for it allows you to manually specify the +# licensing information +[[licenses.clarify]] +# The name of the crate the clarification applies to +name = "encoding_rs" +# The optional version constraint for the crate +#version = "*" +# The SPDX expression for the license requirements of the crate +expression = "(Apache-2.0 OR MIT) AND BSD-3-Clause" +# One or more files in the crate's source used as the "source of truth" for +# the license expression. If the contents match, the clarification will be used +# when running the license check, otherwise the clarification will be ignored +# and the crate will be checked normally, which may produce warnings or errors +# depending on the rest of your configuration +license-files = [ + # Each entry is a crate relative path, and the (opaque) hash of its contents + { path = "COPYRIGHT", hash = 0x39f8ad31 } +] + +[licenses.private] +# If true, ignores workspace crates that aren't published, or are only +# published to private registries +ignore = false +# One or more private registries that you might publish crates to, if a crate +# is only published to private registries, and ignore is true, the crate will +# not have its license(s) checked +registries = [ + #"https://sekretz.com/registry +] + +# This section is considered when running `cargo deny check bans`. +# More documentation about the 'bans' section can be found here: +# https://embarkstudios.github.io/cargo-deny/checks/bans/cfg.html +[bans] +# Lint level for when multiple versions of the same crate are detected +multiple-versions = "warn" +# Lint level for when a crate version requirement is `*` +wildcards = "allow" +# The graph highlighting used when creating dotgraphs for crates +# with multiple versions +# * lowest-version - The path to the lowest versioned duplicate is highlighted +# * simplest-path - The path to the version with the fewest edges is highlighted +# * all - Both lowest-version and simplest-path are used +highlight = "all" +# List of crates that are allowed. Use with care! +allow = [ + #{ name = "ansi_term", version = "=0.11.0" }, +] +# List of crates to deny +deny = [ + # Each entry the name of a crate and a version range. If version is + # not specified, all versions will be matched. + #{ name = "ansi_term", version = "=0.11.0" }, + # + # Wrapper crates can optionally be specified to allow the crate when it + # is a direct dependency of the otherwise banned crate + #{ name = "ansi_term", version = "=0.11.0", wrappers = [] }, +] +# Certain crates/versions that will be skipped when doing duplicate detection. +skip = [ + #{ name = "ansi_term", version = "=0.11.0" }, +] +# Similarly to `skip` allows you to skip certain crates during duplicate +# detection. Unlike skip, it also includes the entire tree of transitive +# dependencies starting at the specified crate, up to a certain depth, which is +# by default infinite +skip-tree = [ + #{ name = "ansi_term", version = "=0.11.0", depth = 20 }, +] + +# This section is considered when running `cargo deny check sources`. +# More documentation about the 'sources' section can be found here: +# https://embarkstudios.github.io/cargo-deny/checks/sources/cfg.html +[sources] +# Lint level for what to happen when a crate from a crate registry that is not +# in the allow list is encountered +unknown-registry = "warn" +# Lint level for what to happen when a crate from a git repository that is not +# in the allow list is encountered +unknown-git = "warn" +# List of URLs for allowed crate registries. Defaults to the crates.io index +# if not specified. If it is specified but empty, no registries are allowed. +allow-registry = ["https://github.com/rust-lang/crates.io-index"] +# List of URLs for allowed Git repositories +allow-git = [] + +[sources.allow-org] +# 1 or more github.com organizations to allow git sources for +#github = [""] +# 1 or more gitlab.com organizations to allow git sources for +#gitlab = [""] +# 1 or more bitbucket.org organizations to allow git sources for +#bitbucket = [""] diff --git a/rustfmt.toml b/rustfmt.toml new file mode 100644 index 0000000..0a9eda5 --- /dev/null +++ b/rustfmt.toml @@ -0,0 +1,8 @@ +fn_single_line = true +group_imports = "StdExternalCrate" +imports_granularity = "Crate" +overflow_delimited_expr = true +reorder_impl_items = true +use_field_init_shorthand = true +use_small_heuristics = "Max" +where_single_line = true diff --git a/src/argh_version.rs b/src/argh_version.rs new file mode 100644 index 0000000..59a29d9 --- /dev/null +++ b/src/argh_version.rs @@ -0,0 +1,65 @@ +// From https://gist.github.com/suluke/e0c672492126be0a4f3b4f0e1115d77c +//! Extend `argh` to be better integrated with the `cargo` ecosystem +//! +//! For now, this only adds a --version/-V option which causes early-exit. +use argh::{FromArgs, TopLevelCommand}; + +struct ArgsOrVersion(T); +impl TopLevelCommand for ArgsOrVersion where T: FromArgs {} +impl FromArgs for ArgsOrVersion +where T: FromArgs +{ + fn from_args(command_name: &[&str], args: &[&str]) -> Result { + /// Also use argh for catching `--version`-only invocations + #[derive(FromArgs)] + struct Version { + /// print version information and exit + #[argh(switch, short = 'V')] + pub version: bool, + } + match Version::from_args(command_name, args) { + Ok(v) => { + if v.version { + Err(argh::EarlyExit { + output: format!( + "{} {} {}", + command_name.first().unwrap_or(&""), + env!("VERGEN_BUILD_SEMVER"), + env!("VERGEN_GIT_SHA"), + ), + status: Ok(()), + }) + } else { + // seems args are empty + T::from_args(command_name, args).map(Self) + } + } + Err(exit) => match exit.status { + Ok(()) => { + // must have been --help + let help = match T::from_args(command_name, &["--help"]) { + Ok(_) => unreachable!(), + Err(exit) => exit.output, + }; + Err(argh::EarlyExit { + output: format!( + "{} -V, --version print version information and exit", + help + ), + status: Ok(()), + }) + } + Err(()) => T::from_args(command_name, args).map(Self), + }, + } + } +} + +/// Create a `FromArgs` type from the current process’s `env::args`. +/// +/// This function will exit early from the current process if argument parsing was unsuccessful or if information like `--help` was requested. +/// Error messages will be printed to stderr, and `--help` output to stdout. +pub fn from_env() -> T +where T: TopLevelCommand { + argh::from_env::>().0 +} diff --git a/src/cmd/demangle.rs b/src/cmd/demangle.rs new file mode 100644 index 0000000..1a8f813 --- /dev/null +++ b/src/cmd/demangle.rs @@ -0,0 +1,26 @@ +use anyhow::{Error, Result}; +use argh::FromArgs; +use cwdemangle::{demangle, DemangleOptions}; + +#[derive(FromArgs, PartialEq, Eq, Debug)] +/// Demangle a CodeWarrior C++ symbol. +#[argh(subcommand, name = "demangle")] +pub struct Args { + #[argh(positional)] + /// symbol to demangle + symbol: String, + #[argh(switch)] + /// disable replacing `(void)` with `()` + keep_void: bool, +} + +pub fn run(args: Args) -> Result<()> { + let options = DemangleOptions { omit_empty_parameters: !args.keep_void }; + match demangle(args.symbol.as_str(), &options) { + Some(symbol) => { + println!("{}", symbol); + Ok(()) + } + None => Err(Error::msg("Failed to demangle symbol")), + } +} diff --git a/src/cmd/elf2dol.rs b/src/cmd/elf2dol.rs new file mode 100644 index 0000000..ae73af9 --- /dev/null +++ b/src/cmd/elf2dol.rs @@ -0,0 +1,173 @@ +use std::{ + fs::File, + io::{BufWriter, Seek, SeekFrom, Write}, +}; + +use anyhow::{Context, Error, Result}; +use argh::FromArgs; +use memmap2::MmapOptions; +use object::{Architecture, Object, ObjectKind, ObjectSection, SectionKind}; + +#[derive(FromArgs, PartialEq, Eq, Debug)] +/// Converts an ELF file to a DOL file. +#[argh(subcommand, name = "elf2dol")] +pub struct Args { + #[argh(positional)] + /// path to input ELF + elf_file: String, + #[argh(positional)] + /// path to output DOL + dol_file: String, +} + +#[derive(Debug, Clone, Default)] +pub struct DolSection { + pub offset: u32, + pub address: u32, + pub size: u32, +} + +#[derive(Debug, Clone, Default)] +pub struct DolHeader { + pub text_sections: Vec, + pub data_sections: Vec, + pub bss_address: u32, + pub bss_size: u32, + pub entry_point: u32, +} + +const MAX_TEXT_SECTIONS: usize = 7; +const MAX_DATA_SECTIONS: usize = 11; +const ZERO_BUF: [u8; 32] = [0u8; 32]; + +pub fn run(args: Args) -> Result<()> { + let elf_file = File::open(&args.elf_file) + .with_context(|| format!("Failed to open ELF file '{}'", args.elf_file))?; + let map = unsafe { MmapOptions::new().map(&elf_file) } + .with_context(|| format!("Failed to mmap binary: '{}'", args.elf_file))?; + let obj_file = object::read::File::parse(&*map)?; + match obj_file.architecture() { + Architecture::PowerPc => {} + arch => return Err(Error::msg(format!("Unexpected architecture: {:?}", arch))), + }; + if obj_file.is_little_endian() { + return Err(Error::msg("Expected big endian")); + } + match obj_file.kind() { + ObjectKind::Executable => {} + kind => return Err(Error::msg(format!("Unexpected ELF type: {:?}", kind))), + } + + let mut out = BufWriter::new( + File::create(&args.dol_file) + .with_context(|| format!("Failed to create DOL file '{}'", args.dol_file))?, + ); + let mut header = DolHeader { entry_point: obj_file.entry() as u32, ..Default::default() }; + let mut offset = 0x100u32; + + // Text sections + for section in obj_file.sections() { + if section.kind() != SectionKind::Text { + continue; + } + let address = section.address() as u32; + let size = align32(section.size() as u32); + header.text_sections.push(DolSection { offset, address, size }); + out.seek(SeekFrom::Start(offset as u64))?; + write_aligned(&mut out, section.data()?)?; + offset += size; + } + + // Data sections + for section in obj_file.sections() { + if section.kind() != SectionKind::Data && section.kind() != SectionKind::ReadOnlyData { + continue; + } + let address = section.address() as u32; + let size = align32(section.size() as u32); + header.data_sections.push(DolSection { offset, address, size }); + out.seek(SeekFrom::Start(offset as u64))?; + write_aligned(&mut out, section.data()?)?; + offset += size; + } + + // BSS sections + for section in obj_file.sections() { + if section.kind() != SectionKind::UninitializedData { + continue; + } + let address = section.address() as u32; + let size = section.size() as u32; + if header.bss_address == 0 { + header.bss_address = address; + } + header.bss_size = (address + size) - header.bss_address; + } + + if header.text_sections.len() > MAX_TEXT_SECTIONS { + return Err(Error::msg(format!( + "Too many text sections: {} / {}", + header.text_sections.len(), + MAX_TEXT_SECTIONS + ))); + } + if header.data_sections.len() > MAX_DATA_SECTIONS { + return Err(Error::msg(format!( + "Too many data sections: {} / {}", + header.data_sections.len(), + MAX_DATA_SECTIONS + ))); + } + + // Offsets + out.rewind()?; + for section in &header.text_sections { + out.write_all(§ion.offset.to_be_bytes())?; + } + out.seek(SeekFrom::Start(0x1c))?; + for section in &header.data_sections { + out.write_all(§ion.offset.to_be_bytes())?; + } + + // Addresses + out.seek(SeekFrom::Start(0x48))?; + for section in &header.text_sections { + out.write_all(§ion.address.to_be_bytes())?; + } + out.seek(SeekFrom::Start(0x64))?; + for section in &header.data_sections { + out.write_all(§ion.address.to_be_bytes())?; + } + + // Sizes + out.seek(SeekFrom::Start(0x90))?; + for section in &header.text_sections { + out.write_all(§ion.size.to_be_bytes())?; + } + out.seek(SeekFrom::Start(0xac))?; + for section in &header.data_sections { + out.write_all(§ion.size.to_be_bytes())?; + } + + // BSS + entry + out.seek(SeekFrom::Start(0xd8))?; + out.write_all(&header.bss_address.to_be_bytes())?; + out.write_all(&header.bss_size.to_be_bytes())?; + out.write_all(&header.entry_point.to_be_bytes())?; + + Ok(()) +} + +#[inline] +fn align32(x: u32) -> u32 { (x + 31) & !31 } + +#[inline] +fn write_aligned(out: &mut T, bytes: &[u8]) -> std::io::Result<()> { + let len = bytes.len() as u32; + let padding = align32(len) - len; + out.write_all(bytes)?; + if padding > 0 { + out.write_all(&ZERO_BUF[0..padding as usize])?; + } + Ok(()) +} diff --git a/src/cmd/map.rs b/src/cmd/map.rs new file mode 100644 index 0000000..bc8184c --- /dev/null +++ b/src/cmd/map.rs @@ -0,0 +1,140 @@ +use std::{fs::File, io::BufReader}; + +use anyhow::{Context, Error, Result}; +use argh::FromArgs; + +use crate::util::map::{process_map, SymbolEntry, SymbolRef}; + +#[derive(FromArgs, PartialEq, Debug)] +/// Commands for processing CodeWarrior maps. +#[argh(subcommand, name = "map")] +pub struct Args { + #[argh(subcommand)] + command: SubCommand, +} + +#[derive(FromArgs, PartialEq, Debug)] +#[argh(subcommand)] +enum SubCommand { + Entries(EntriesArgs), + Symbol(SymbolArgs), +} + +#[derive(FromArgs, PartialEq, Eq, Debug)] +/// Displays all entries for a particular TU. +#[argh(subcommand, name = "entries")] +pub struct EntriesArgs { + #[argh(positional)] + /// path to input map + map_file: String, + #[argh(positional)] + /// TU to display entries for + unit: String, +} + +#[derive(FromArgs, PartialEq, Eq, Debug)] +/// Displays all references to a symbol. +#[argh(subcommand, name = "symbol")] +pub struct SymbolArgs { + #[argh(positional)] + /// path to input map + map_file: String, + #[argh(positional)] + /// symbol to display references for + symbol: String, +} + +pub fn run(args: Args) -> Result<()> { + match args.command { + SubCommand::Entries(c_args) => entries(c_args), + SubCommand::Symbol(c_args) => symbol(c_args), + } +} + +fn entries(args: EntriesArgs) -> Result<()> { + let reader = BufReader::new( + File::open(&args.map_file) + .with_context(|| format!("Failed to open file '{}'", args.map_file))?, + ); + let entries = process_map(reader)?; + match entries.unit_entries.get_vec(&args.unit) { + Some(vec) => { + for symbol_ref in vec { + if symbol_ref.name.starts_with('@') { + continue; + } + if let Some(symbol) = entries.symbols.get(symbol_ref) { + println!("{}", symbol.demangled.as_ref().unwrap_or(&symbol.name)); + } else { + println!("Symbol not found: {}", symbol_ref.name); + } + } + } + None => { + return Err(Error::msg(format!( + "Failed to find entries for TU '{}' in map", + args.unit + ))); + } + } + Ok(()) +} + +fn symbol(args: SymbolArgs) -> Result<()> { + let reader = BufReader::new( + File::open(&args.map_file) + .with_context(|| format!("Failed to open file '{}'", args.map_file))?, + ); + let entries = process_map(reader)?; + let mut opt_ref: Option<(SymbolRef, SymbolEntry)> = None; + for (symbol_ref, entry) in &entries.symbols { + if symbol_ref.name == args.symbol { + if opt_ref.is_some() { + return Err(Error::msg(format!("Symbol '{}' found in multiple TUs", args.symbol))); + } + opt_ref = Some((symbol_ref.clone(), entry.clone())); + } + } + match opt_ref { + Some((symbol_ref, symbol)) => { + println!("Located symbol {}", symbol.demangled.as_ref().unwrap_or(&symbol.name)); + if let Some(vec) = entries.entry_references.get_vec(&symbol_ref) { + println!("\nReferences:"); + for x in vec { + if let Some(reference) = entries.symbols.get(x) { + println!( + ">>> {} ({:?},{:?}) [{}]", + reference.demangled.as_ref().unwrap_or(&reference.name), + reference.kind, + reference.visibility, + reference.unit + ); + } else { + println!(">>> {} (NOT FOUND)", x.name); + } + } + } + if let Some(vec) = entries.entry_referenced_from.get_vec(&symbol_ref) { + println!("\nReferenced from:"); + for x in vec { + if let Some(reference) = entries.symbols.get(x) { + println!( + ">>> {} ({:?}, {:?}) [{}]", + reference.demangled.as_ref().unwrap_or(&reference.name), + reference.kind, + reference.visibility, + reference.unit + ); + } else { + println!(">>> {} (NOT FOUND)", x.name); + } + } + } + println!("\n"); + } + None => { + return Err(Error::msg(format!("Failed to find symbol '{}' in map", args.symbol))); + } + } + Ok(()) +} diff --git a/src/cmd/metroidbuildinfo.rs b/src/cmd/metroidbuildinfo.rs new file mode 100644 index 0000000..f2190a9 --- /dev/null +++ b/src/cmd/metroidbuildinfo.rs @@ -0,0 +1,47 @@ +use anyhow::{Context, Error, Result}; +use argh::FromArgs; +use memchr::memmem; +use memmap2::MmapOptions; + +#[derive(FromArgs, PartialEq, Eq, Debug)] +/// Sets the MetroidBuildInfo tag value in a given binary. +#[argh(subcommand, name = "metroidbuildinfo")] +pub struct Args { + #[argh(positional)] + /// path to source binary + binary: String, + #[argh(positional)] + /// path to build info string + build_info: String, +} + +const BUILD_STRING_MAX: usize = 35; +const BUILD_STRING_TAG: &str = "!#$MetroidBuildInfo!#$"; + +pub fn run(args: Args) -> Result<()> { + let build_string = std::fs::read_to_string(&args.build_info) + .with_context(|| format!("Failed to read build info string from '{}'", args.build_info))?; + let build_string_trim = build_string.trim_end(); + if build_string_trim.as_bytes().len() > BUILD_STRING_MAX { + return Err(Error::msg(format!( + "Build string '{}' is greater than maximum size of {}", + build_string_trim, BUILD_STRING_MAX + ))); + } + + let binary_file = std::fs::File::options() + .read(true) + .write(true) + .open(&args.binary) + .with_context(|| format!("Failed to open binary for writing: '{}'", args.binary))?; + let mut map = unsafe { MmapOptions::new().map_mut(&binary_file) } + .with_context(|| format!("Failed to mmap binary: '{}'", args.binary))?; + let start = match memmem::find(&map, BUILD_STRING_TAG.as_bytes()) { + Some(idx) => idx + BUILD_STRING_TAG.as_bytes().len(), + None => return Err(Error::msg("Failed to find build string tag in binary")), + }; + let end = start + build_string_trim.as_bytes().len(); + map[start..end].copy_from_slice(build_string_trim.as_bytes()); + map[end] = 0; + Ok(()) +} diff --git a/src/cmd/mod.rs b/src/cmd/mod.rs new file mode 100644 index 0000000..7119d38 --- /dev/null +++ b/src/cmd/mod.rs @@ -0,0 +1,5 @@ +pub(crate) mod demangle; +pub(crate) mod elf2dol; +pub(crate) mod map; +pub(crate) mod metroidbuildinfo; +pub(crate) mod shasum; diff --git a/src/cmd/shasum.rs b/src/cmd/shasum.rs new file mode 100644 index 0000000..ede2a94 --- /dev/null +++ b/src/cmd/shasum.rs @@ -0,0 +1,88 @@ +use std::{ + fs::File, + io::{BufRead, BufReader, Read}, +}; + +use anyhow::{Context, Error, Result}; +use argh::FromArgs; +use sha1::{Digest, Sha1}; + +#[derive(FromArgs, PartialEq, Eq, Debug)] +/// Print or check SHA1 (160-bit) checksums. +#[argh(subcommand, name = "shasum")] +pub struct Args { + #[argh(switch, short = 'c')] + /// check SHA sums against given list + check: bool, + #[argh(positional)] + /// path to file + file: String, +} + +const DEFAULT_BUF_SIZE: usize = 8192; + +pub fn run(args: Args) -> Result<()> { + let file = + File::open(&args.file).with_context(|| format!("Failed to open file '{}'", args.file))?; + if args.check { + check(args, file) + } else { + hash(args, file) + } +} + +fn check(_args: Args, file: File) -> Result<()> { + let reader = BufReader::new(file); + let mut mismatches = 0usize; + for line in reader.lines() { + let line = match line { + Ok(line) => line, + Err(e) => return Err(Error::msg(format!("File read failed: {}", e))), + }; + let (hash, file_name) = + line.split_once(' ').ok_or_else(|| Error::msg(format!("Invalid line: {}", line)))?; + let file_name = match file_name.chars().next() { + Some(' ') | Some('*') => &file_name[1..], + _ => return Err(Error::msg(format!("Invalid line: {}", line))), + }; + let mut hash_bytes = [0u8; 20]; + hex::decode_to_slice(hash, &mut hash_bytes) + .with_context(|| format!("Invalid line: {}", line))?; + + let file = File::open(file_name) + .with_context(|| format!("Failed to open file '{}'", file_name))?; + let found_hash = file_sha1(file)?; + if hash_bytes == found_hash.as_ref() { + println!("{}: OK", file_name); + } else { + println!("{}: FAILED", file_name); + mismatches += 1; + } + } + if mismatches != 0 { + eprintln!("WARNING: {} computed checksum did NOT match", mismatches); + std::process::exit(1); + } + Ok(()) +} + +fn hash(args: Args, file: File) -> Result<()> { + let hash = file_sha1(file)?; + let mut hash_buf = [0u8; 40]; + let hash_str = base16ct::lower::encode_str(&hash, &mut hash_buf) + .map_err(|e| Error::msg(format!("Failed to encode hash: {}", e)))?; + println!("{} {}", hash_str, args.file); + Ok(()) +} + +fn file_sha1(mut file: File) -> Result> { + let mut buf = [0u8; DEFAULT_BUF_SIZE]; + let mut hasher = Sha1::new(); + Ok(loop { + let read = file.read(&mut buf).context("File read failed")?; + if read == 0 { + break hasher.finalize(); + } + hasher.update(&buf[0..read]); + }) +} diff --git a/src/main.rs b/src/main.rs new file mode 100644 index 0000000..46a09c1 --- /dev/null +++ b/src/main.rs @@ -0,0 +1,39 @@ +extern crate core; + +use argh::FromArgs; + +mod argh_version; +mod cmd; +mod util; + +#[derive(FromArgs, PartialEq, Debug)] +/// GameCube/Wii decompilation project tools. +struct TopLevel { + #[argh(subcommand)] + command: SubCommand, +} + +#[derive(FromArgs, PartialEq, Debug)] +#[argh(subcommand)] +enum SubCommand { + Demangle(cmd::demangle::Args), + Elf2Dol(cmd::elf2dol::Args), + Map(cmd::map::Args), + MetroidBuildInfo(cmd::metroidbuildinfo::Args), + Shasum(cmd::shasum::Args), +} + +fn main() { + let args: TopLevel = argh_version::from_env(); + let result = match args.command { + SubCommand::Demangle(c_args) => cmd::demangle::run(c_args), + SubCommand::Elf2Dol(c_args) => cmd::elf2dol::run(c_args), + SubCommand::Map(c_args) => cmd::map::run(c_args), + SubCommand::MetroidBuildInfo(c_args) => cmd::metroidbuildinfo::run(c_args), + SubCommand::Shasum(c_args) => cmd::shasum::run(c_args), + }; + if let Err(e) = result { + eprintln!("{:?}", e); + std::process::exit(1); + } +} diff --git a/src/util/map.rs b/src/util/map.rs new file mode 100644 index 0000000..727624f --- /dev/null +++ b/src/util/map.rs @@ -0,0 +1,485 @@ +use std::{ + collections::{btree_map::Entry, BTreeMap, HashMap}, + io::BufRead, + ops::Range, +}; + +use anyhow::{Error, Result}; +use cwdemangle::{demangle, DemangleOptions}; +use lazy_static::lazy_static; +use multimap::MultiMap; +use regex::Regex; +use topological_sort::TopologicalSort; + +#[derive(Debug, Copy, Clone, Eq, PartialEq)] +pub enum SymbolKind { + Function, + Object, + Section, + NoType, +} + +#[derive(Debug, Copy, Clone, Eq, PartialEq)] +pub enum SymbolVisibility { + Global, + Local, + Weak, +} + +#[derive(Debug, Clone)] +pub struct SymbolEntry { + pub name: String, + pub demangled: Option, + pub kind: SymbolKind, + pub visibility: SymbolVisibility, + pub unit: String, + pub address: u32, + pub size: u32, + pub section: String, +} + +#[derive(Debug, Clone, Hash, Eq, PartialEq)] +pub struct SymbolRef { + pub name: String, + pub unit: String, +} + +#[derive(Default)] +struct SectionOrder { + symbol_order: Vec, + unit_order: Vec<(String, Vec)>, +} + +fn is_code_section(section: &str) -> bool { section == ".text" || section == ".init" } + +/// Iterate over the BTreeMap and generate an ordered list of symbols and TUs by address. +fn resolve_section_order( + address_to_symbol: &BTreeMap, + symbol_entries: &mut HashMap, +) -> Result { + let mut ordering = SectionOrder::default(); + + let mut last_unit = String::new(); + let mut unit_override = String::new(); + let mut last_section = String::new(); + let mut section_unit_idx = 0usize; + for symbol_ref in address_to_symbol.values() { + if let Some(symbol) = symbol_entries.get_mut(symbol_ref) { + if last_unit != symbol.unit { + unit_override.clear(); + + if last_section != symbol.section { + ordering.unit_order.push((symbol.section.clone(), vec![])); + section_unit_idx = ordering.unit_order.len() - 1; + last_section = symbol.section.clone(); + } + let unit_order = &mut ordering.unit_order[section_unit_idx]; + if unit_order.1.contains(&symbol.unit) { + // With -common on, .bss is split into two parts. The TU order repeats + // at the end with all globally-deduplicated BSS symbols. Once we detect + // a duplicate inside of .bss, we create a new section and start again. + // TODO the first entry in .comm *could* be a TU without regular .bss + if symbol.section == ".bss" { + log::debug!(".comm section detected, duplicate {}", symbol.unit); + ordering.unit_order.push((".comm".to_string(), vec![symbol.unit.clone()])); + section_unit_idx = ordering.unit_order.len() - 1; + } else { + // Since the map doesn't contain file paths, it's likely that + // a TU name conflict is simply a separate file. + // TODO need to resolve and split unit in other sections as well + unit_override = + format!("{}_{}_{:X}", symbol.unit, symbol.section, symbol.address); + log::warn!( + "TU order conflict: {} exists multiple times in {}. Renaming to {}.", + symbol.unit, + symbol.section, + unit_override, + ); + unit_order.1.push(unit_override.clone()); + } + } else { + unit_order.1.push(symbol.unit.clone()); + } + last_unit = symbol.unit.clone(); + } + // For ASM-generated objects, notype,local symbols in .text + // are usually local jump labels, and should be ignored. + if is_code_section(&symbol.section) + && symbol.size == 0 + && symbol.kind == SymbolKind::NoType + && symbol.visibility == SymbolVisibility::Local + { + // Being named something other than lbl_* could indicate + // that it's actually a local function, but let's just + // make the user resolve that if necessary. + if !symbol.name.starts_with("lbl_") { + log::warn!("Skipping local text symbol {}", symbol.name); + } + continue; + } + // Guess the symbol type if necessary. + if symbol.kind == SymbolKind::NoType { + if is_code_section(&symbol.section) { + symbol.kind = SymbolKind::Function; + } else { + symbol.kind = SymbolKind::Object; + } + } + // If we're renaming this TU, replace it in the symbol. + if !unit_override.is_empty() { + symbol.unit = unit_override.clone(); + } + ordering.symbol_order.push(symbol_ref.clone()); + } else { + return Err(Error::msg(format!("Symbol has address but no entry: {:?}", symbol_ref))); + } + } + + for iter in ordering.symbol_order.windows(2) { + let next_address = symbol_entries.get(&iter[1]).unwrap().address; + let symbol = symbol_entries.get_mut(&iter[0]).unwrap(); + // For ASM-generated objects, we need to guess the symbol size. + if symbol.size == 0 { + symbol.size = next_address - symbol.address; + } + } + + Ok(ordering) +} + +/// The ordering of TUs inside of each section represents a directed edge in a DAG. +/// We can use a topological sort to determine a valid global TU order. +/// There can be ambiguities, but any solution that satisfies the link order +/// constraints is considered valid. +// TODO account for library ordering +#[allow(dead_code)] +pub fn resolve_link_order(section_unit_order: &[(String, Vec)]) -> Result> { + let mut global_unit_order = Vec::::new(); + let mut t_sort = TopologicalSort::::new(); + for (section, order) in section_unit_order { + let mut order: &[String] = order; + if (section == ".ctors" || section == ".dtors") && order.len() > 1 { + // __init_cpp_exceptions.o has symbols that get ordered to the beginning of + // .ctors and .dtors, so our topological sort would fail if we added them. + // Always skip the first TU of .ctors and .dtors. + order = &order[1..]; + } + for iter in order.windows(2) { + t_sort.add_dependency(iter[0].clone(), iter[1].clone()); + } + } + for unit in &mut t_sort { + global_unit_order.push(unit); + } + // An incomplete topological sort indicates that a cyclic dependency was encountered. + if !t_sort.is_empty() { + return Err(Error::msg("Cyclic dependency encountered!")); + } + // Sanity check, did we get all TUs in the final order? + for (_, order) in section_unit_order { + for unit in order { + if !global_unit_order.contains(unit) { + return Err(Error::msg(format!("Failed to find an order for {}", unit))); + } + } + } + Ok(global_unit_order) +} + +lazy_static! { + static ref LINK_MAP_START: Regex = Regex::new("^Link map of (.*)$").unwrap(); + static ref LINK_MAP_ENTRY: Regex = Regex::new( + "^\\s*(?P\\d+)] (?P.*) \\((?P.*),(?P.*)\\) found in (?P.*)$", + ) + .unwrap(); + static ref LINK_MAP_ENTRY_GENERATED: Regex = + Regex::new("^\\s*(?P\\d+)] (?P.*) found as linker generated symbol$").unwrap(); + static ref LINK_MAP_ENTRY_DUPLICATE: Regex = + Regex::new("^\\s*(?P\\d+)] >>> UNREFERENCED DUPLICATE (?P.*)$").unwrap(); + static ref SECTION_LAYOUT_START: Regex = Regex::new("^(?P
.*) section layout$").unwrap(); + static ref SECTION_LAYOUT_SYMBOL: Regex = Regex::new( + "^\\s*(?P[0-9A-Fa-f]+|UNUSED)\\s+(?P[0-9A-Fa-f]+)\\s+(?P[0-9A-Fa-f]+|\\.{8})\\s+(?P\\d+)?\\s*(?P.*?)(?:\\s+\\(entry of (?P.*?)\\))?\\s+(?P.*)$", + ) + .unwrap(); + static ref SECTION_LAYOUT_HEADER: Regex = Regex::new( + "^(\\s*Starting\\s+Virtual\\s*|\\s*address\\s+Size\\s+address\\s*|\\s*-----------------------\\s*)$", + ) + .unwrap(); + static ref MEMORY_MAP_HEADER: Regex = Regex::new("^\\s*Memory map:\\s*$").unwrap(); + static ref EXTERN_SYMBOL: Regex = Regex::new("^\\s*>>> SYMBOL NOT FOUND: (.*)$").unwrap(); +} + +#[derive(Default)] +pub struct MapEntries { + pub symbols: HashMap, + pub unit_entries: MultiMap, + pub entry_references: MultiMap, + pub entry_referenced_from: MultiMap, + pub address_to_symbol: BTreeMap, + pub unit_section_ranges: HashMap>, + pub symbol_order: Vec, + pub unit_order: Vec<(String, Vec)>, +} + +pub fn process_map(reader: R) -> Result { + let mut entries = MapEntries::default(); + + let mut symbol_stack = Vec::::new(); + let mut current_section = String::new(); + let mut last_name = String::new(); + let mut last_unit = String::new(); + let mut has_link_map = false; + let mut relative_offset = 0u32; + let mut last_section_end = 0u32; + for result in reader.lines() { + match result { + Ok(line) => { + if let Some(captures) = LINK_MAP_START.captures(&line) { + log::debug!("Entry point: {}", &captures[1]); + has_link_map = true; + } else if let Some(captures) = LINK_MAP_ENTRY.captures(&line) { + if captures["sym"].starts_with('.') { + last_name.clear(); + continue; + } + let is_duplicate = &captures["sym"] == ">>>"; + let unit = captures["tu"].trim().to_string(); + let name = if is_duplicate { + if last_name.is_empty() { + return Err(Error::msg("Last name empty?")); + } + last_name.clone() + } else { + captures["sym"].to_string() + }; + let symbol_ref = SymbolRef { name: name.clone(), unit: unit.clone() }; + let depth: usize = captures["depth"].parse()?; + if depth > symbol_stack.len() { + symbol_stack.push(symbol_ref.clone()); + } else if depth <= symbol_stack.len() { + symbol_stack.truncate(depth - 1); + symbol_stack.push(symbol_ref.clone()); + } + // println!("Entry: {} ({})", name, tu); + let kind = match &captures["type"] { + "func" => SymbolKind::Function, + "object" => SymbolKind::Object, + "section" => SymbolKind::Section, + "notype" => SymbolKind::NoType, + _ => { + return Err(Error::msg(format!( + "Unknown symbol type: {}", + &captures["type"], + ))); + } + }; + let visibility = match &captures["vis"] { + "global" => SymbolVisibility::Global, + "local" => SymbolVisibility::Local, + "weak" => SymbolVisibility::Weak, + _ => { + return Err(Error::msg(format!( + "Unknown symbol visibility: {}", + &captures["vis"], + ))); + } + }; + if !is_duplicate && symbol_stack.len() > 1 { + let from = &symbol_stack[symbol_stack.len() - 2]; + entries.entry_referenced_from.insert(symbol_ref.clone(), from.clone()); + entries.entry_references.insert(from.clone(), symbol_ref.clone()); + } + let mut should_insert = true; + if let Some(symbol) = entries.symbols.get(&symbol_ref) { + if symbol.kind != kind { + log::warn!( + "Kind mismatch for {}: was {:?}, now {:?}", + symbol.name, + symbol.kind, + kind + ); + } + if symbol.visibility != visibility { + log::warn!( + "Visibility mismatch for {}: was {:?}, now {:?}", + symbol.name, + symbol.visibility, + visibility + ); + } + entries.unit_entries.insert(unit.clone(), symbol_ref.clone()); + should_insert = false; + } + if should_insert { + let demangled = + demangle(&name, &DemangleOptions { omit_empty_parameters: true }); + entries.symbols.insert(symbol_ref.clone(), SymbolEntry { + name: name.clone(), + demangled, + kind, + visibility, + unit: unit.clone(), + address: 0, + size: 0, + section: String::new(), + }); + last_name = name.clone(); + entries.unit_entries.insert(unit, symbol_ref.clone()); + } + } else if let Some(captures) = LINK_MAP_ENTRY_GENERATED.captures(&line) { + let name = captures["sym"].to_string(); + let demangled = + demangle(&name, &DemangleOptions { omit_empty_parameters: true }); + let symbol_ref = + SymbolRef { name: name.clone(), unit: "[generated]".to_string() }; + entries.symbols.insert(symbol_ref, SymbolEntry { + name, + demangled, + kind: SymbolKind::NoType, + visibility: SymbolVisibility::Global, + unit: "[generated]".to_string(), + address: 0, + size: 0, + section: String::new(), + }); + } else if line.trim().is_empty() + || LINK_MAP_ENTRY_DUPLICATE.is_match(&line) + || SECTION_LAYOUT_HEADER.is_match(&line) + || EXTERN_SYMBOL.is_match(&line) + { + // Ignore + } else if let Some(captures) = SECTION_LAYOUT_START.captures(&line) { + current_section = captures["section"].trim().to_string(); + last_unit.clear(); + log::debug!("Processing section layout for {}", current_section); + } else if let Some(captures) = SECTION_LAYOUT_SYMBOL.captures(&line) { + if captures["rom_addr"].trim() == "UNUSED" { + continue; + } + let sym_name = captures["sym"].trim(); + let tu = captures["tu"].trim(); + let mut address = u32::from_str_radix(captures["addr"].trim(), 16)?; + let mut size = u32::from_str_radix(captures["size"].trim(), 16)?; + + // For RELs, the each section starts at address 0. For our purposes + // we'll create "fake" addresses by simply starting at the end of the + // previous section. + if last_unit.is_empty() { + if address == 0 { + relative_offset = last_section_end; + } else { + relative_offset = 0; + } + } + address += relative_offset; + + // Section symbol (i.e. ".data") indicates section size for a TU + if sym_name == current_section { + // Skip empty sections + if size == 0 { + continue; + } + let end = address + size; + entries.unit_section_ranges.insert(tu.to_string(), address..end); + last_unit = tu.to_string(); + last_section_end = end; + continue; + } + + // Otherwise, for ASM-generated objects, the first section symbol in a TU + // has the full size of the section. + if tu != last_unit { + if size == 0 { + return Err(Error::msg(format!( + "No section size for {} in {}", + sym_name, tu + ))); + } + let end = address + size; + entries.unit_section_ranges.insert(tu.to_string(), address..end); + last_unit = tu.to_string(); + last_section_end = end; + + // Clear it, so that we guess the "real" symbol size later. + size = 0; + } + + // Ignore ...data.0 and similar + if sym_name.starts_with("...") { + continue; + } + + let symbol_ref = SymbolRef { name: sym_name.to_string(), unit: tu.to_string() }; + if let Some(symbol) = entries.symbols.get_mut(&symbol_ref) { + symbol.address = address; + symbol.size = size; + symbol.section = current_section.clone(); + match entries.address_to_symbol.entry(address) { + Entry::Vacant(entry) => { + entry.insert(symbol_ref.clone()); + } + Entry::Occupied(entry) => { + log::warn!( + "Symbol overridden @ {:X} from {} to {} in {}", + symbol.address, + entry.get().name, + sym_name, + tu + ); + } + } + } else { + let visibility = if has_link_map { + log::warn!( + "Symbol not in link map: {} ({}). Type and visibility unknown.", + sym_name, + tu, + ); + SymbolVisibility::Local + } else { + SymbolVisibility::Global + }; + entries.symbols.insert(symbol_ref.clone(), SymbolEntry { + name: sym_name.to_string(), + demangled: None, + kind: SymbolKind::NoType, + visibility, + unit: tu.to_string(), + address, + size, + section: current_section.clone(), + }); + match entries.address_to_symbol.entry(address) { + Entry::Vacant(entry) => { + entry.insert(symbol_ref.clone()); + } + Entry::Occupied(entry) => { + log::warn!( + "Symbol overridden @ {:X} from {} to {} in {}", + address, + entry.get().name, + sym_name, + tu + ); + } + } + } + } else if MEMORY_MAP_HEADER.is_match(&line) { + // log::debug!("Done"); + break; + } else { + todo!("{}", line); + } + } + Err(e) => { + return Err(Error::from(e)); + } + } + } + + let section_order = resolve_section_order(&entries.address_to_symbol, &mut entries.symbols)?; + entries.symbol_order = section_order.symbol_order; + entries.unit_order = section_order.unit_order; + + Ok(entries) +} diff --git a/src/util/mod.rs b/src/util/mod.rs new file mode 100644 index 0000000..2a9657b --- /dev/null +++ b/src/util/mod.rs @@ -0,0 +1 @@ +pub(crate) mod map;