mirror of
https://github.com/encounter/objdiff.git
synced 2025-12-16 16:37:06 +00:00
Compare commits
53 Commits
v2.0.0-alp
...
v2.0.0-bet
| Author | SHA1 | Date | |
|---|---|---|---|
| e1ae369d17 | |||
| ce05d6d6c0 | |||
| c16a926d9b | |||
|
|
a32d99923c | ||
| 68606dfdcb | |||
| b4650b660a | |||
| 195379968c | |||
|
|
3bd8aaee41 | ||
| 1f4175dc21 | |||
| 0fccae1049 | |||
| 8250d26b77 | |||
| fd555a6e0f | |||
| 3710b6a91e | |||
| faebddbc5e | |||
| a733a950a3 | |||
| cad9b70632 | |||
| cf937b0be9 | |||
| 23b6d33a98 | |||
| f17ee83622 | |||
| 615ec4c50a | |||
| 2cc10b0d06 | |||
| 8091941448 | |||
| de74dfdba7 | |||
| 177bd5e895 | |||
| e1ccee1e73 | |||
| 952b6a63c3 | |||
|
|
09cc9952df | ||
| fc598af329 | |||
| 871407622d | |||
| e3fff7b0dc | |||
|
|
75b0e7d9e5 | ||
|
|
9f71ce9fea | ||
|
|
d9fb48853e | ||
| 233839346a | |||
| 95615c2ec5 | |||
|
|
97981160f4 | ||
|
|
1fd901a863 | ||
| 759d55994a | |||
| 9710ccc38a | |||
| 79cd460333 | |||
|
|
a5a6a3928e | ||
| fc54e93681 | |||
| c9b11db2fa | |||
|
|
b991960080 | ||
| 425dc8546b | |||
| 9e04357d9f | |||
| 6037c12ad0 | |||
| b15f643713 | |||
| 3f82c1a50f | |||
| 0ea6242669 | |||
| 0c20a0d9cd | |||
| f30b3cfae2 | |||
| 9e57a66a05 |
109
.github/workflows/build.yaml
vendored
109
.github/workflows/build.yaml
vendored
@@ -25,6 +25,21 @@ jobs:
|
||||
sudo apt-get -y install libgtk-3-dev
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
- name: Check git tag against Cargo version
|
||||
if: startsWith(github.ref, 'refs/tags/')
|
||||
shell: bash
|
||||
run: |
|
||||
set -eou pipefail
|
||||
tag='${{github.ref}}'
|
||||
tag="${tag#refs/tags/}"
|
||||
for file in */Cargo.toml; do
|
||||
version=$(grep '^version' $file | head -1 | awk -F' = ' '{print $2}' | tr -d '"')
|
||||
version="v$version"
|
||||
if [ "$tag" != "$version" ]; then
|
||||
echo "::error::Git tag doesn't match the Cargo version! ($tag != $version)"
|
||||
exit 1
|
||||
fi
|
||||
done
|
||||
- name: Setup Rust toolchain
|
||||
uses: dtolnay/rust-toolchain@stable
|
||||
with:
|
||||
@@ -100,8 +115,82 @@ jobs:
|
||||
SCCACHE_GHA_ENABLED: "true"
|
||||
run: cargo test --release
|
||||
|
||||
build:
|
||||
name: Build
|
||||
build-cli:
|
||||
name: Build objdiff-cli
|
||||
env:
|
||||
CARGO_BIN_NAME: objdiff-cli
|
||||
strategy:
|
||||
matrix:
|
||||
include:
|
||||
- platform: ubuntu-latest
|
||||
target: x86_64-unknown-linux-musl
|
||||
name: linux-x86_64
|
||||
build: zigbuild
|
||||
features: default
|
||||
- platform: ubuntu-latest
|
||||
target: i686-unknown-linux-musl
|
||||
name: linux-i686
|
||||
build: zigbuild
|
||||
features: default
|
||||
- platform: ubuntu-latest
|
||||
target: aarch64-unknown-linux-musl
|
||||
name: linux-aarch64
|
||||
build: zigbuild
|
||||
features: default
|
||||
- platform: ubuntu-latest
|
||||
target: armv7-unknown-linux-musleabi
|
||||
name: linux-armv7l
|
||||
build: zigbuild
|
||||
features: default
|
||||
- platform: windows-latest
|
||||
target: x86_64-pc-windows-msvc
|
||||
name: windows-x86_64
|
||||
build: build
|
||||
features: default
|
||||
- platform: windows-latest
|
||||
target: aarch64-pc-windows-msvc
|
||||
name: windows-arm64
|
||||
build: build
|
||||
features: default
|
||||
- platform: macos-latest
|
||||
target: x86_64-apple-darwin
|
||||
name: macos-x86_64
|
||||
build: build
|
||||
features: default
|
||||
- platform: macos-latest
|
||||
target: aarch64-apple-darwin
|
||||
name: macos-arm64
|
||||
build: build
|
||||
features: default
|
||||
fail-fast: false
|
||||
runs-on: ${{ matrix.platform }}
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
- name: Install cargo-zigbuild
|
||||
if: matrix.build == 'zigbuild'
|
||||
run: pip install ziglang==0.13.0 cargo-zigbuild==0.19.1
|
||||
- name: Setup Rust toolchain
|
||||
uses: dtolnay/rust-toolchain@stable
|
||||
with:
|
||||
targets: ${{ matrix.target }}
|
||||
- name: Cargo build
|
||||
run: >
|
||||
cargo ${{ matrix.build }} --profile ${{ env.BUILD_PROFILE }} --target ${{ matrix.target }}
|
||||
--bin ${{ env.CARGO_BIN_NAME }} --features ${{ matrix.features }}
|
||||
- name: Upload artifacts
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: ${{ env.CARGO_BIN_NAME }}-${{ matrix.name }}
|
||||
path: |
|
||||
${{ env.CARGO_TARGET_DIR }}/${{ matrix.target }}/${{ env.BUILD_PROFILE }}/${{ env.CARGO_BIN_NAME }}
|
||||
${{ env.CARGO_TARGET_DIR }}/${{ matrix.target }}/${{ env.BUILD_PROFILE }}/${{ env.CARGO_BIN_NAME }}.exe
|
||||
if-no-files-found: error
|
||||
|
||||
build-gui:
|
||||
name: Build objdiff-gui
|
||||
env:
|
||||
CARGO_BIN_NAME: objdiff
|
||||
strategy:
|
||||
matrix:
|
||||
include:
|
||||
@@ -144,23 +233,21 @@ jobs:
|
||||
SCCACHE_GHA_ENABLED: "true"
|
||||
run: >
|
||||
cargo build --profile ${{ env.BUILD_PROFILE }} --target ${{ matrix.target }}
|
||||
--bin objdiff-cli --bin objdiff --features ${{ matrix.features }}
|
||||
--bin ${{ env.CARGO_BIN_NAME }} --features ${{ matrix.features }}
|
||||
- name: Upload artifacts
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: ${{ matrix.name }}
|
||||
name: ${{ env.CARGO_BIN_NAME }}-${{ matrix.name }}
|
||||
path: |
|
||||
${{ env.CARGO_TARGET_DIR }}/${{ matrix.target }}/${{ env.BUILD_PROFILE }}/objdiff-cli
|
||||
${{ env.CARGO_TARGET_DIR }}/${{ matrix.target }}/${{ env.BUILD_PROFILE }}/objdiff-cli.exe
|
||||
${{ env.CARGO_TARGET_DIR }}/${{ matrix.target }}/${{ env.BUILD_PROFILE }}/objdiff
|
||||
${{ env.CARGO_TARGET_DIR }}/${{ matrix.target }}/${{ env.BUILD_PROFILE }}/objdiff.exe
|
||||
${{ env.CARGO_TARGET_DIR }}/${{ matrix.target }}/${{ env.BUILD_PROFILE }}/${{ env.CARGO_BIN_NAME }}
|
||||
${{ env.CARGO_TARGET_DIR }}/${{ matrix.target }}/${{ env.BUILD_PROFILE }}/${{ 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 ]
|
||||
needs: [ check, build-cli, build-gui ]
|
||||
permissions:
|
||||
contents: write
|
||||
steps:
|
||||
@@ -183,7 +270,9 @@ jobs:
|
||||
else
|
||||
ext=".$ext"
|
||||
fi
|
||||
dst="../out/${name}-${dir%/}${ext}"
|
||||
arch="${dir%/}" # remove trailing slash
|
||||
arch="${arch##"$name-"}" # remove bin name
|
||||
dst="../out/${name}-${arch}${ext}"
|
||||
mv "$file" "$dst"
|
||||
done
|
||||
done
|
||||
|
||||
1
.gitignore
vendored
1
.gitignore
vendored
@@ -22,3 +22,4 @@ android.keystore
|
||||
*.frag
|
||||
*.vert
|
||||
*.metal
|
||||
.vscode/launch.json
|
||||
|
||||
980
Cargo.lock
generated
980
Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
75
README.md
75
README.md
@@ -14,9 +14,10 @@ Features:
|
||||
- Click to highlight all instances of values and registers.
|
||||
|
||||
Supports:
|
||||
- PowerPC 750CL (GameCube & Wii)
|
||||
- MIPS (Nintendo 64 & PS2)
|
||||
- PowerPC 750CL (GameCube, Wii)
|
||||
- MIPS (N64, PS1, PS2, PSP)
|
||||
- x86 (COFF only at the moment)
|
||||
- ARM (GBA, DS, 3DS)
|
||||
|
||||
See [Usage](#usage) for more information.
|
||||
|
||||
@@ -48,91 +49,89 @@ See [Configuration](#configuration) for more information.
|
||||
|
||||
## Configuration
|
||||
|
||||
While **not required** (most settings can be specified in the UI), projects can add an `objdiff.json` (or
|
||||
`objdiff.yaml`, `objdiff.yml`) file to configure the tool automatically. The configuration file must be located in
|
||||
While **not required** (most settings can be specified in the UI), projects can add an `objdiff.json` file to configure the tool automatically. The configuration file must be located in
|
||||
the root project directory.
|
||||
|
||||
If your project has a generator script (e.g. `configure.py`), it's recommended to generate the objdiff configuration
|
||||
file as well. You can then add `objdiff.json` to your `.gitignore` to prevent it from being committed.
|
||||
|
||||
```json5
|
||||
// objdiff.json
|
||||
```json
|
||||
{
|
||||
"$schema": "https://raw.githubusercontent.com/encounter/objdiff/main/config.schema.json",
|
||||
"custom_make": "ninja",
|
||||
"custom_args": [
|
||||
"-d",
|
||||
"keeprsp"
|
||||
],
|
||||
|
||||
// Only required if objects use "path" instead of "target_path" and "base_path".
|
||||
"target_dir": "build/asm",
|
||||
"base_dir": "build/src",
|
||||
|
||||
"build_target": true,
|
||||
"build_target": false,
|
||||
"build_base": true,
|
||||
"watch_patterns": [
|
||||
"*.c",
|
||||
"*.cp",
|
||||
"*.cpp",
|
||||
"*.cxx",
|
||||
"*.h",
|
||||
"*.hp",
|
||||
"*.hpp",
|
||||
"*.py"
|
||||
"*.hxx",
|
||||
"*.s",
|
||||
"*.S",
|
||||
"*.asm",
|
||||
"*.inc",
|
||||
"*.py",
|
||||
"*.yml",
|
||||
"*.txt",
|
||||
"*.json"
|
||||
],
|
||||
"objects": [
|
||||
"units": [
|
||||
{
|
||||
"name": "main/MetroTRK/mslsupp",
|
||||
|
||||
// Option 1: Relative to target_dir and base_dir
|
||||
"path": "MetroTRK/mslsupp.o",
|
||||
// Option 2: Explicit paths from project root
|
||||
// Useful for more complex directory layouts
|
||||
"target_path": "build/asm/MetroTRK/mslsupp.o",
|
||||
"base_path": "build/src/MetroTRK/mslsupp.o",
|
||||
|
||||
"reverse_fn_order": false
|
||||
},
|
||||
// ...
|
||||
"metadata": {}
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
### Schema
|
||||
|
||||
View [config.schema.json](config.schema.json) for all available options. The below list is a summary of the most important options.
|
||||
|
||||
`custom_make` _(optional)_: By default, objdiff will use `make` to build the project.
|
||||
If the project uses a different build system (e.g. `ninja`), specify it here.
|
||||
The build command will be `[custom_make] [custom_args] path/to/object.o`.
|
||||
|
||||
`custom_args` _(optional)_: Additional arguments to pass to the build command prior to the object path.
|
||||
|
||||
`target_dir` _(optional)_: Relative from the root of the project, this where the "target" or "expected" objects are located.
|
||||
These are the **intended result** of the match.
|
||||
|
||||
`base_dir` _(optional)_: Relative from the root of the project, this is where the "base" or "actual" objects are located.
|
||||
These are objects built from the **current source code**.
|
||||
|
||||
`build_target`: If true, objdiff will tell the build system to build the target objects before diffing (e.g.
|
||||
`make path/to/target.o`).
|
||||
This is useful if the target objects are not built by default or can change based on project configuration or edits
|
||||
to assembly files.
|
||||
Requires the build system to be configured properly.
|
||||
|
||||
`build_base`: If true, objdiff will tell the build system to build the base objects before diffing (e.g. `make path/to/base.o`).
|
||||
It's unlikely you'll want to disable this, unless you're using an external tool to rebuild the base object on source file changes.
|
||||
|
||||
`watch_patterns` _(optional)_: A list of glob patterns to watch for changes.
|
||||
([Supported syntax](https://docs.rs/globset/latest/globset/#syntax))
|
||||
If any of these files change, objdiff will automatically rebuild the objects and re-compare them.
|
||||
If not specified, objdiff will use the default patterns listed above.
|
||||
|
||||
`objects` _(optional)_: If specified, objdiff will display a list of objects in the sidebar for easy navigation.
|
||||
`units` _(optional)_: If specified, objdiff will display a list of objects in the sidebar for easy navigation.
|
||||
|
||||
> `name` _(optional)_: The name of the object in the UI. If not specified, the object's `path` will be used.
|
||||
>
|
||||
> `path`: Relative path to the object from the `target_dir` and `base_dir`.
|
||||
> Requires `target_dir` and `base_dir` to be specified.
|
||||
> `target_path`: Path to the "target" or "expected" object from the project root.
|
||||
> This object is the **intended result** of the match.
|
||||
>
|
||||
> `target_path`: Path to the target object from the project root.
|
||||
> Required if `path` is not specified.
|
||||
> `base_path`: Path to the "base" or "actual" object from the project root.
|
||||
> This object is built from the **current source code**.
|
||||
>
|
||||
> `base_path`: Path to the base object from the project root.
|
||||
> Required if `path` is not specified.
|
||||
> `metadata.auto_generated` _(optional)_: Hides the object from the object list, but still includes it in reports.
|
||||
>
|
||||
> `reverse_fn_order` _(optional)_: Displays function symbols in reversed order.
|
||||
Used to support MWCC's `-inline deferred` option, which reverses the order of functions in the object file.
|
||||
> `metadata.complete` _(optional)_: Marks the object as "complete" (or "linked") in the object list.
|
||||
> This is useful for marking objects that are fully decompiled. A value of `false` will mark the object as "incomplete".
|
||||
|
||||
## Building
|
||||
|
||||
|
||||
221
config.schema.json
Normal file
221
config.schema.json
Normal file
@@ -0,0 +1,221 @@
|
||||
{
|
||||
"$schema": "https://json-schema.org/draft-07/schema",
|
||||
"$id": "https://raw.githubusercontent.com/encounter/objdiff/main/config.schema.json",
|
||||
"title": "objdiff configuration",
|
||||
"description": "Configuration file for objdiff",
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"min_version": {
|
||||
"type": "string",
|
||||
"description": "Minimum version of objdiff required to load this configuration file.",
|
||||
"examples": [
|
||||
"1.0.0",
|
||||
"2.0.0-beta.1"
|
||||
]
|
||||
},
|
||||
"custom_make": {
|
||||
"type": "string",
|
||||
"description": "By default, objdiff will use make to build the project.\nIf the project uses a different build system (e.g. ninja), specify it here.\nThe build command will be `[custom_make] [custom_args] path/to/object.o`.",
|
||||
"examples": [
|
||||
"make",
|
||||
"ninja"
|
||||
],
|
||||
"default": "make"
|
||||
},
|
||||
"custom_args": {
|
||||
"type": "array",
|
||||
"description": "Additional arguments to pass to the build command prior to the object path.",
|
||||
"items": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"target_dir": {
|
||||
"type": "string",
|
||||
"description": "Relative from the root of the project, this where the \"target\" or \"expected\" objects are located.\nThese are the intended result of the match.",
|
||||
"deprecated": true
|
||||
},
|
||||
"base_dir": {
|
||||
"type": "string",
|
||||
"description": "Relative from the root of the project, this is where the \"base\" or \"actual\" objects are located.\nThese are objects built from the current source code.",
|
||||
"deprecated": true
|
||||
},
|
||||
"build_target": {
|
||||
"type": "boolean",
|
||||
"description": "If true, objdiff will tell the build system to build the target objects before diffing (e.g. `make path/to/target.o`).\nThis is useful if the target objects are not built by default or can change based on project configuration or edits to assembly files.\nRequires the build system to be configured properly.",
|
||||
"default": false
|
||||
},
|
||||
"build_base": {
|
||||
"type": "boolean",
|
||||
"description": "If true, objdiff will tell the build system to build the base objects before diffing (e.g. `make path/to/base.o`).\nIt's unlikely you'll want to disable this, unless you're using an external tool to rebuild the base object on source file changes.",
|
||||
"default": true
|
||||
},
|
||||
"watch_patterns": {
|
||||
"type": "array",
|
||||
"description": "List of glob patterns to watch for changes in the project.\nIf any of these files change, objdiff will automatically rebuild the objects and re-compare them.\nSupported syntax: https://docs.rs/globset/latest/globset/#syntax",
|
||||
"items": {
|
||||
"type": "string"
|
||||
},
|
||||
"default": [
|
||||
"*.c",
|
||||
"*.cp",
|
||||
"*.cpp",
|
||||
"*.cxx",
|
||||
"*.h",
|
||||
"*.hp",
|
||||
"*.hpp",
|
||||
"*.hxx",
|
||||
"*.s",
|
||||
"*.S",
|
||||
"*.asm",
|
||||
"*.inc",
|
||||
"*.py",
|
||||
"*.yml",
|
||||
"*.txt",
|
||||
"*.json"
|
||||
]
|
||||
},
|
||||
"objects": {
|
||||
"type": "array",
|
||||
"description": "Use units instead.",
|
||||
"deprecated": true,
|
||||
"items": {
|
||||
"$ref": "#/$defs/unit"
|
||||
}
|
||||
},
|
||||
"units": {
|
||||
"type": "array",
|
||||
"description": "If specified, objdiff will display a list of objects in the sidebar for easy navigation.",
|
||||
"items": {
|
||||
"$ref": "#/$defs/unit"
|
||||
}
|
||||
},
|
||||
"progress_categories": {
|
||||
"type": "array",
|
||||
"description": "Progress categories used for objdiff-cli report.",
|
||||
"items": {
|
||||
"$ref": "#/$defs/progress_category"
|
||||
}
|
||||
}
|
||||
},
|
||||
"$defs": {
|
||||
"unit": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"name": {
|
||||
"type": "string",
|
||||
"description": "The name of the object in the UI. If not specified, the object's path will be used."
|
||||
},
|
||||
"path": {
|
||||
"type": "string",
|
||||
"description": "Relative path to the object from the target_dir and base_dir.\nRequires target_dir and base_dir to be specified.",
|
||||
"deprecated": true
|
||||
},
|
||||
"target_path": {
|
||||
"type": "string",
|
||||
"description": "Path to the target object from the project root.\nRequired if path is not specified."
|
||||
},
|
||||
"base_path": {
|
||||
"type": "string",
|
||||
"description": "Path to the base object from the project root.\nRequired if path is not specified."
|
||||
},
|
||||
"reverse_fn_order": {
|
||||
"type": "boolean",
|
||||
"description": "Displays function symbols in reversed order.\nUsed to support MWCC's -inline deferred option, which reverses the order of functions in the object file.",
|
||||
"deprecated": true
|
||||
},
|
||||
"complete": {
|
||||
"type": "boolean",
|
||||
"description": "Marks the object as \"complete\" (or \"linked\") in the object list.\nThis is useful for marking objects that are fully decompiled. A value of `false` will mark the object as \"incomplete\".",
|
||||
"deprecated": true
|
||||
},
|
||||
"scratch": {
|
||||
"ref": "#/$defs/scratch"
|
||||
},
|
||||
"metadata": {
|
||||
"ref": "#/$defs/metadata"
|
||||
}
|
||||
}
|
||||
},
|
||||
"scratch": {
|
||||
"type": "object",
|
||||
"description": "If present, objdiff will display a button to create a decomp.me scratch.",
|
||||
"properties": {
|
||||
"platform": {
|
||||
"type": "string",
|
||||
"description": "The decomp.me platform ID to use for the scratch.",
|
||||
"examples": [
|
||||
"gc_wii",
|
||||
"n64"
|
||||
]
|
||||
},
|
||||
"compiler": {
|
||||
"type": "string",
|
||||
"description": "The decomp.me compiler ID to use for the scratch.",
|
||||
"examples": [
|
||||
"mwcc_242_81",
|
||||
"ido7.1"
|
||||
]
|
||||
},
|
||||
"c_flags": {
|
||||
"type": "string",
|
||||
"description": "C flags to use for the scratch. Exclude any include paths."
|
||||
},
|
||||
"ctx_path": {
|
||||
"type": "string",
|
||||
"description": "Path to the context file to use for the scratch."
|
||||
},
|
||||
"build_ctx": {
|
||||
"type": "boolean",
|
||||
"description": "If true, objdiff will run the build command with the context file as an argument to generate it.",
|
||||
"default": false
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"platform",
|
||||
"compiler"
|
||||
]
|
||||
},
|
||||
"metadata": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"complete": {
|
||||
"type": "boolean",
|
||||
"description": "Marks the object as \"complete\" (or \"linked\") in the object list.\nThis is useful for marking objects that are fully decompiled. A value of `false` will mark the object as \"incomplete\"."
|
||||
},
|
||||
"reverse_fn_order": {
|
||||
"type": "boolean",
|
||||
"description": "Displays function symbols in reversed order.\nUsed to support MWCC's -inline deferred option, which reverses the order of functions in the object file."
|
||||
},
|
||||
"source_path": {
|
||||
"type": "string",
|
||||
"description": "Path to the source file that generated the object."
|
||||
},
|
||||
"progress_categories": {
|
||||
"type": "array",
|
||||
"description": "Progress categories used for objdiff-cli report.",
|
||||
"items": {
|
||||
"type": "string",
|
||||
"description": "Unique identifier for the category. (See progress_categories)"
|
||||
}
|
||||
},
|
||||
"auto_generated": {
|
||||
"type": "boolean",
|
||||
"description": "Hides the object from the object list by default, but still includes it in reports."
|
||||
}
|
||||
}
|
||||
},
|
||||
"progress_category": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"id": {
|
||||
"type": "string",
|
||||
"description": "Unique identifier for the category."
|
||||
},
|
||||
"name": {
|
||||
"type": "string",
|
||||
"description": "Human-readable name of the category."
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "objdiff-cli"
|
||||
version = "2.0.0-alpha.1"
|
||||
version = "2.0.0-beta.6"
|
||||
edition = "2021"
|
||||
rust-version = "1.70"
|
||||
authors = ["Luke Street <luke@street.dev>"]
|
||||
@@ -18,7 +18,9 @@ anyhow = "1.0.82"
|
||||
argp = "0.3.0"
|
||||
crossterm = "0.27.0"
|
||||
enable-ansi-support = "0.2.1"
|
||||
memmap2 = "0.9.4"
|
||||
objdiff-core = { path = "../objdiff-core", features = ["all"] }
|
||||
prost = "0.13.1"
|
||||
ratatui = "0.26.2"
|
||||
rayon = "1.10.0"
|
||||
serde = { version = "1", features = ["derive"] }
|
||||
|
||||
@@ -1,4 +1,9 @@
|
||||
use std::{fs, io::stdout, path::PathBuf, str::FromStr};
|
||||
use std::{
|
||||
fs,
|
||||
io::stdout,
|
||||
path::{Path, PathBuf},
|
||||
str::FromStr,
|
||||
};
|
||||
|
||||
use anyhow::{bail, Context, Result};
|
||||
use argp::FromArgs;
|
||||
@@ -14,6 +19,7 @@ use crossterm::{
|
||||
};
|
||||
use event::KeyModifiers;
|
||||
use objdiff_core::{
|
||||
bindings::diff::DiffResult,
|
||||
config::{ProjectConfig, ProjectObject},
|
||||
diff,
|
||||
diff::{
|
||||
@@ -28,10 +34,13 @@ use ratatui::{
|
||||
widgets::{Block, Borders, Clear, Paragraph, Scrollbar, ScrollbarOrientation, ScrollbarState},
|
||||
};
|
||||
|
||||
use crate::util::term::crossterm_panic_handler;
|
||||
use crate::util::{
|
||||
output::{write_output, OutputFormat},
|
||||
term::crossterm_panic_handler,
|
||||
};
|
||||
|
||||
#[derive(FromArgs, PartialEq, Debug)]
|
||||
/// Diff two object files.
|
||||
/// Diff two object files. (Interactive or one-shot mode)
|
||||
#[argp(subcommand, name = "diff")]
|
||||
pub struct Args {
|
||||
#[argp(option, short = '1')]
|
||||
@@ -49,101 +58,152 @@ pub struct Args {
|
||||
#[argp(switch, short = 'x')]
|
||||
/// Relax relocation diffs
|
||||
relax_reloc_diffs: bool,
|
||||
#[argp(option, short = 'o')]
|
||||
/// Output file (one-shot mode) ("-" for stdout)
|
||||
output: Option<PathBuf>,
|
||||
#[argp(option)]
|
||||
/// Output format (json, json-pretty, proto) (default: json)
|
||||
format: Option<String>,
|
||||
#[argp(positional)]
|
||||
/// Function symbol to diff
|
||||
symbol: String,
|
||||
symbol: Option<String>,
|
||||
}
|
||||
|
||||
pub fn run(args: Args) -> Result<()> {
|
||||
let (target_path, base_path, project_config) =
|
||||
match (&args.target, &args.base, &args.project, &args.unit) {
|
||||
(Some(t), Some(b), None, None) => (Some(t.clone()), Some(b.clone()), None),
|
||||
(None, None, p, u) => {
|
||||
let project = match p {
|
||||
Some(project) => project.clone(),
|
||||
_ => std::env::current_dir().context("Failed to get the current directory")?,
|
||||
let (target_path, base_path, project_config) = match (
|
||||
&args.target,
|
||||
&args.base,
|
||||
&args.project,
|
||||
&args.unit,
|
||||
) {
|
||||
(Some(t), Some(b), None, None) => (Some(t.clone()), Some(b.clone()), None),
|
||||
(None, None, p, u) => {
|
||||
let project = match p {
|
||||
Some(project) => project.clone(),
|
||||
_ => std::env::current_dir().context("Failed to get the current directory")?,
|
||||
};
|
||||
let Some((project_config, project_config_info)) =
|
||||
objdiff_core::config::try_project_config(&project)
|
||||
else {
|
||||
bail!("Project config not found in {}", &project.display())
|
||||
};
|
||||
let mut project_config = project_config.with_context(|| {
|
||||
format!("Reading project config {}", project_config_info.path.display())
|
||||
})?;
|
||||
let object = {
|
||||
let resolve_paths = |o: &mut ProjectObject| {
|
||||
o.resolve_paths(
|
||||
&project,
|
||||
project_config.target_dir.as_deref(),
|
||||
project_config.base_dir.as_deref(),
|
||||
)
|
||||
};
|
||||
let Some((project_config, project_config_info)) =
|
||||
objdiff_core::config::try_project_config(&project)
|
||||
else {
|
||||
bail!("Project config not found in {}", &project.display())
|
||||
};
|
||||
let mut project_config = project_config.with_context(|| {
|
||||
format!("Reading project config {}", project_config_info.path.display())
|
||||
})?;
|
||||
let object = {
|
||||
let resolve_paths = |o: &mut ProjectObject| {
|
||||
o.resolve_paths(
|
||||
&project,
|
||||
project_config.target_dir.as_deref(),
|
||||
project_config.base_dir.as_deref(),
|
||||
)
|
||||
};
|
||||
if let Some(u) = u {
|
||||
let unit_path =
|
||||
PathBuf::from_str(u).ok().and_then(|p| fs::canonicalize(p).ok());
|
||||
|
||||
let Some(object) = project_config.objects.iter_mut().find_map(|obj| {
|
||||
if obj.name.as_deref() == Some(u) {
|
||||
resolve_paths(obj);
|
||||
return Some(obj);
|
||||
}
|
||||
|
||||
let up = unit_path.as_deref()?;
|
||||
if let Some(u) = u {
|
||||
let unit_path =
|
||||
PathBuf::from_str(u).ok().and_then(|p| fs::canonicalize(p).ok());
|
||||
|
||||
let Some(object) = project_config.objects.iter_mut().find_map(|obj| {
|
||||
if obj.name.as_deref() == Some(u) {
|
||||
resolve_paths(obj);
|
||||
|
||||
if [&obj.base_path, &obj.target_path]
|
||||
.into_iter()
|
||||
.filter_map(|p| p.as_ref().and_then(|p| p.canonicalize().ok()))
|
||||
.any(|p| p == up)
|
||||
{
|
||||
return Some(obj);
|
||||
}
|
||||
|
||||
None
|
||||
}) else {
|
||||
bail!("Unit not found: {}", u)
|
||||
};
|
||||
|
||||
object
|
||||
} else {
|
||||
let mut idx = None;
|
||||
let mut count = 0usize;
|
||||
for (i, obj) in project_config.objects.iter_mut().enumerate() {
|
||||
resolve_paths(obj);
|
||||
|
||||
if obj
|
||||
.target_path
|
||||
.as_deref()
|
||||
.map(|o| obj::read::has_function(o, &args.symbol))
|
||||
.transpose()?
|
||||
.unwrap_or(false)
|
||||
{
|
||||
idx = Some(i);
|
||||
count += 1;
|
||||
if count > 1 {
|
||||
break;
|
||||
}
|
||||
}
|
||||
return Some(obj);
|
||||
}
|
||||
match (count, idx) {
|
||||
(0, None) => bail!("Symbol not found: {}", &args.symbol),
|
||||
(1, Some(i)) => &mut project_config.objects[i],
|
||||
(2.., Some(_)) => bail!(
|
||||
"Multiple instances of {} were found, try specifying a unit",
|
||||
&args.symbol
|
||||
),
|
||||
_ => unreachable!(),
|
||||
|
||||
let up = unit_path.as_deref()?;
|
||||
|
||||
resolve_paths(obj);
|
||||
|
||||
if [&obj.base_path, &obj.target_path]
|
||||
.into_iter()
|
||||
.filter_map(|p| p.as_ref().and_then(|p| p.canonicalize().ok()))
|
||||
.any(|p| p == up)
|
||||
{
|
||||
return Some(obj);
|
||||
}
|
||||
|
||||
None
|
||||
}) else {
|
||||
bail!("Unit not found: {}", u)
|
||||
};
|
||||
|
||||
object
|
||||
} else if let Some(symbol_name) = &args.symbol {
|
||||
let mut idx = None;
|
||||
let mut count = 0usize;
|
||||
for (i, obj) in project_config.objects.iter_mut().enumerate() {
|
||||
resolve_paths(obj);
|
||||
|
||||
if obj
|
||||
.target_path
|
||||
.as_deref()
|
||||
.map(|o| obj::read::has_function(o, symbol_name))
|
||||
.transpose()?
|
||||
.unwrap_or(false)
|
||||
{
|
||||
idx = Some(i);
|
||||
count += 1;
|
||||
if count > 1 {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
let target_path = object.target_path.clone();
|
||||
let base_path = object.base_path.clone();
|
||||
(target_path, base_path, Some(project_config))
|
||||
}
|
||||
_ => bail!("Either target and base or project and unit must be specified"),
|
||||
};
|
||||
match (count, idx) {
|
||||
(0, None) => bail!("Symbol not found: {}", symbol_name),
|
||||
(1, Some(i)) => &mut project_config.objects[i],
|
||||
(2.., Some(_)) => bail!(
|
||||
"Multiple instances of {} were found, try specifying a unit",
|
||||
symbol_name
|
||||
),
|
||||
_ => unreachable!(),
|
||||
}
|
||||
} else {
|
||||
bail!("Must specify one of: symbol, project and unit, target and base objects")
|
||||
}
|
||||
};
|
||||
let target_path = object.target_path.clone();
|
||||
let base_path = object.base_path.clone();
|
||||
(target_path, base_path, Some(project_config))
|
||||
}
|
||||
_ => bail!("Either target and base or project and unit must be specified"),
|
||||
};
|
||||
|
||||
if let Some(output) = &args.output {
|
||||
run_oneshot(&args, output, target_path.as_deref(), base_path.as_deref())
|
||||
} else {
|
||||
run_interactive(args, target_path, base_path, project_config)
|
||||
}
|
||||
}
|
||||
|
||||
fn run_oneshot(
|
||||
args: &Args,
|
||||
output: &Path,
|
||||
target_path: Option<&Path>,
|
||||
base_path: Option<&Path>,
|
||||
) -> Result<()> {
|
||||
let output_format = OutputFormat::from_option(args.format.as_deref())?;
|
||||
let config = diff::DiffObjConfig {
|
||||
relax_reloc_diffs: args.relax_reloc_diffs,
|
||||
..Default::default() // TODO
|
||||
};
|
||||
let target = target_path
|
||||
.map(|p| obj::read::read(p, &config).with_context(|| format!("Loading {}", p.display())))
|
||||
.transpose()?;
|
||||
let base = base_path
|
||||
.map(|p| obj::read::read(p, &config).with_context(|| format!("Loading {}", p.display())))
|
||||
.transpose()?;
|
||||
let result = diff::diff_objs(&config, target.as_ref(), base.as_ref(), None)?;
|
||||
let left = target.as_ref().and_then(|o| result.left.as_ref().map(|d| (o, d)));
|
||||
let right = base.as_ref().and_then(|o| result.right.as_ref().map(|d| (o, d)));
|
||||
write_output(&DiffResult::new(left, right), Some(output), output_format)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn run_interactive(
|
||||
args: Args,
|
||||
target_path: Option<PathBuf>,
|
||||
base_path: Option<PathBuf>,
|
||||
project_config: Option<ProjectConfig>,
|
||||
) -> Result<()> {
|
||||
let Some(symbol_name) = &args.symbol else { bail!("Interactive mode requires a symbol name") };
|
||||
let time_format = time::format_description::parse_borrowed::<2>("[hour]:[minute]:[second]")
|
||||
.context("Failed to parse time format")?;
|
||||
let mut state = Box::new(FunctionDiffUi {
|
||||
@@ -156,7 +216,7 @@ pub fn run(args: Args) -> Result<()> {
|
||||
scroll_state_y: ScrollbarState::default(),
|
||||
per_page: 0,
|
||||
num_rows: 0,
|
||||
symbol_name: args.symbol.clone(),
|
||||
symbol_name: symbol_name.clone(),
|
||||
target_path,
|
||||
base_path,
|
||||
project_config,
|
||||
@@ -180,7 +240,7 @@ pub fn run(args: Args) -> Result<()> {
|
||||
stdout(),
|
||||
EnterAlternateScreen,
|
||||
EnableMouseCapture,
|
||||
SetTitle(format!("{} - objdiff", args.symbol)),
|
||||
SetTitle(format!("{} - objdiff", symbol_name)),
|
||||
)?;
|
||||
let backend = CrosstermBackend::new(stdout());
|
||||
let mut terminal = Terminal::new(backend)?;
|
||||
@@ -750,8 +810,11 @@ impl FunctionDiffUi {
|
||||
base_color = COLOR_ROTATION[diff.idx % COLOR_ROTATION.len()]
|
||||
}
|
||||
}
|
||||
DiffText::BranchDest(addr) => {
|
||||
DiffText::BranchDest(addr, diff) => {
|
||||
label_text = format!("{addr:x}");
|
||||
if let Some(diff) = diff {
|
||||
base_color = COLOR_ROTATION[diff.idx % COLOR_ROTATION.len()]
|
||||
}
|
||||
}
|
||||
DiffText::Symbol(sym) => {
|
||||
let name = sym.demangled_name.as_ref().unwrap_or(&sym.name);
|
||||
@@ -809,21 +872,24 @@ impl FunctionDiffUi {
|
||||
|
||||
fn reload(&mut self) -> Result<()> {
|
||||
let prev = self.right_obj.take();
|
||||
let config = diff::DiffObjConfig {
|
||||
relax_reloc_diffs: self.relax_reloc_diffs,
|
||||
..Default::default() // TODO
|
||||
};
|
||||
let target = self
|
||||
.target_path
|
||||
.as_deref()
|
||||
.map(|p| obj::read::read(p).with_context(|| format!("Loading {}", p.display())))
|
||||
.map(|p| {
|
||||
obj::read::read(p, &config).with_context(|| format!("Loading {}", p.display()))
|
||||
})
|
||||
.transpose()?;
|
||||
let base = self
|
||||
.base_path
|
||||
.as_deref()
|
||||
.map(|p| obj::read::read(p).with_context(|| format!("Loading {}", p.display())))
|
||||
.map(|p| {
|
||||
obj::read::read(p, &config).with_context(|| format!("Loading {}", p.display()))
|
||||
})
|
||||
.transpose()?;
|
||||
let config = diff::DiffObjConfig {
|
||||
relax_reloc_diffs: self.relax_reloc_diffs,
|
||||
space_between_args: true, // TODO
|
||||
x86_formatter: Default::default(), // TODO
|
||||
};
|
||||
let result = diff::diff_objs(&config, target.as_ref(), base.as_ref(), prev.as_ref())?;
|
||||
|
||||
let left_sym = target.as_ref().and_then(|o| find_function(o, &self.symbol_name));
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
use std::{
|
||||
collections::HashSet,
|
||||
fs::File,
|
||||
io::{BufReader, BufWriter, Write},
|
||||
io::Read,
|
||||
path::{Path, PathBuf},
|
||||
time::Instant,
|
||||
};
|
||||
@@ -9,15 +9,23 @@ use std::{
|
||||
use anyhow::{bail, Context, Result};
|
||||
use argp::FromArgs;
|
||||
use objdiff_core::{
|
||||
bindings::report::{
|
||||
ChangeItem, ChangeItemInfo, ChangeUnit, Changes, ChangesInput, Measures, Report,
|
||||
ReportCategory, ReportItem, ReportItemMetadata, ReportUnit, ReportUnitMetadata,
|
||||
REPORT_VERSION,
|
||||
},
|
||||
config::ProjectObject,
|
||||
diff, obj,
|
||||
obj::{ObjSectionKind, ObjSymbolFlags},
|
||||
};
|
||||
use prost::Message;
|
||||
use rayon::iter::{IntoParallelRefMutIterator, ParallelIterator};
|
||||
use tracing::{info, warn};
|
||||
|
||||
use crate::util::output::{write_output, OutputFormat};
|
||||
|
||||
#[derive(FromArgs, PartialEq, Debug)]
|
||||
/// Commands for processing NVIDIA Shield TV alf files.
|
||||
/// Generate a progress report for a project.
|
||||
#[argp(subcommand, name = "report")]
|
||||
pub struct Args {
|
||||
#[argp(subcommand)]
|
||||
@@ -32,18 +40,21 @@ pub enum SubCommand {
|
||||
}
|
||||
|
||||
#[derive(FromArgs, PartialEq, Debug)]
|
||||
/// Generate a report from a project.
|
||||
/// Generate a progress report for a project.
|
||||
#[argp(subcommand, name = "generate")]
|
||||
pub struct GenerateArgs {
|
||||
#[argp(option, short = 'p')]
|
||||
/// Project directory
|
||||
project: Option<PathBuf>,
|
||||
#[argp(option, short = 'o')]
|
||||
/// Output JSON file
|
||||
/// Output file
|
||||
output: Option<PathBuf>,
|
||||
#[argp(switch, short = 'd')]
|
||||
/// Deduplicate global and weak symbols (runs single-threaded)
|
||||
deduplicate: bool,
|
||||
#[argp(option, short = 'f')]
|
||||
/// Output format (json, json-pretty, proto) (default: json)
|
||||
format: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(FromArgs, PartialEq, Debug)]
|
||||
@@ -51,65 +62,17 @@ pub struct GenerateArgs {
|
||||
#[argp(subcommand, name = "changes")]
|
||||
pub struct ChangesArgs {
|
||||
#[argp(positional)]
|
||||
/// Previous report JSON file
|
||||
/// Previous report file
|
||||
previous: PathBuf,
|
||||
#[argp(positional)]
|
||||
/// Current report JSON file
|
||||
/// Current report file
|
||||
current: PathBuf,
|
||||
#[argp(option, short = 'o')]
|
||||
/// Output JSON file
|
||||
/// Output file
|
||||
output: Option<PathBuf>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Default, serde::Serialize, serde::Deserialize)]
|
||||
struct Report {
|
||||
fuzzy_match_percent: f32,
|
||||
total_code: u64,
|
||||
matched_code: u64,
|
||||
matched_code_percent: f32,
|
||||
total_data: u64,
|
||||
matched_data: u64,
|
||||
matched_data_percent: f32,
|
||||
total_functions: u32,
|
||||
matched_functions: u32,
|
||||
matched_functions_percent: f32,
|
||||
units: Vec<ReportUnit>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Default, serde::Serialize, serde::Deserialize)]
|
||||
struct ReportUnit {
|
||||
name: String,
|
||||
fuzzy_match_percent: f32,
|
||||
total_code: u64,
|
||||
matched_code: u64,
|
||||
total_data: u64,
|
||||
matched_data: u64,
|
||||
total_functions: u32,
|
||||
matched_functions: u32,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
complete: Option<bool>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
module_name: Option<String>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
module_id: Option<u32>,
|
||||
sections: Vec<ReportItem>,
|
||||
functions: Vec<ReportItem>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Default, serde::Serialize, serde::Deserialize)]
|
||||
struct ReportItem {
|
||||
name: String,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
demangled_name: Option<String>,
|
||||
#[serde(
|
||||
default,
|
||||
skip_serializing_if = "Option::is_none",
|
||||
serialize_with = "serialize_hex",
|
||||
deserialize_with = "deserialize_hex"
|
||||
)]
|
||||
address: Option<u64>,
|
||||
size: u64,
|
||||
fuzzy_match_percent: f32,
|
||||
#[argp(option, short = 'f')]
|
||||
/// Output format (json, json-pretty, proto) (default: json)
|
||||
format: Option<String>,
|
||||
}
|
||||
|
||||
pub fn run(args: Args) -> Result<()> {
|
||||
@@ -120,12 +83,14 @@ pub fn run(args: Args) -> Result<()> {
|
||||
}
|
||||
|
||||
fn generate(args: GenerateArgs) -> Result<()> {
|
||||
let output_format = OutputFormat::from_option(args.format.as_deref())?;
|
||||
let project_dir = args.project.as_deref().unwrap_or_else(|| Path::new("."));
|
||||
info!("Loading project {}", project_dir.display());
|
||||
|
||||
let config = objdiff_core::config::try_project_config(project_dir);
|
||||
let Some((Ok(mut project), _)) = config else {
|
||||
bail!("No project configuration found");
|
||||
let mut project = match objdiff_core::config::try_project_config(project_dir) {
|
||||
Some((Ok(config), _)) => config,
|
||||
Some((Err(err), _)) => bail!("Failed to load project configuration: {}", err),
|
||||
None => bail!("No project configuration found"),
|
||||
};
|
||||
info!(
|
||||
"Generating report for {} units (using {} threads)",
|
||||
@@ -134,7 +99,7 @@ fn generate(args: GenerateArgs) -> Result<()> {
|
||||
);
|
||||
|
||||
let start = Instant::now();
|
||||
let mut report = Report::default();
|
||||
let mut units = vec![];
|
||||
let mut existing_functions: HashSet<String> = HashSet::new();
|
||||
if args.deduplicate {
|
||||
// If deduplicating, we need to run single-threaded
|
||||
@@ -146,11 +111,11 @@ fn generate(args: GenerateArgs) -> Result<()> {
|
||||
project.base_dir.as_deref(),
|
||||
Some(&mut existing_functions),
|
||||
)? {
|
||||
report.units.push(unit);
|
||||
units.push(unit);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
let units = project
|
||||
let vec = project
|
||||
.objects
|
||||
.par_iter_mut()
|
||||
.map(|object| {
|
||||
@@ -163,51 +128,23 @@ fn generate(args: GenerateArgs) -> Result<()> {
|
||||
)
|
||||
})
|
||||
.collect::<Result<Vec<Option<ReportUnit>>>>()?;
|
||||
report.units = units.into_iter().flatten().collect();
|
||||
units = vec.into_iter().flatten().collect();
|
||||
}
|
||||
for unit in &report.units {
|
||||
report.fuzzy_match_percent += unit.fuzzy_match_percent * unit.total_code as f32;
|
||||
report.total_code += unit.total_code;
|
||||
report.matched_code += unit.matched_code;
|
||||
report.total_data += unit.total_data;
|
||||
report.matched_data += unit.matched_data;
|
||||
report.total_functions += unit.total_functions;
|
||||
report.matched_functions += unit.matched_functions;
|
||||
let measures = units.iter().flat_map(|u| u.measures.into_iter()).collect();
|
||||
let mut categories = Vec::new();
|
||||
for category in &project.progress_categories {
|
||||
categories.push(ReportCategory {
|
||||
id: category.id.clone(),
|
||||
name: category.name.clone(),
|
||||
measures: Some(Default::default()),
|
||||
});
|
||||
}
|
||||
if report.total_code == 0 {
|
||||
report.fuzzy_match_percent = 100.0;
|
||||
} else {
|
||||
report.fuzzy_match_percent /= report.total_code as f32;
|
||||
}
|
||||
|
||||
report.matched_code_percent = if report.total_code == 0 {
|
||||
100.0
|
||||
} else {
|
||||
report.matched_code as f32 / report.total_code as f32 * 100.0
|
||||
};
|
||||
report.matched_data_percent = if report.total_data == 0 {
|
||||
100.0
|
||||
} else {
|
||||
report.matched_data as f32 / report.total_data as f32 * 100.0
|
||||
};
|
||||
report.matched_functions_percent = if report.total_functions == 0 {
|
||||
100.0
|
||||
} else {
|
||||
report.matched_functions as f32 / report.total_functions as f32 * 100.0
|
||||
};
|
||||
let mut report =
|
||||
Report { measures: Some(measures), units, version: REPORT_VERSION, categories };
|
||||
report.calculate_progress_categories();
|
||||
let duration = start.elapsed();
|
||||
info!("Report generated in {}.{:03}s", duration.as_secs(), duration.subsec_millis());
|
||||
if let Some(output) = &args.output {
|
||||
info!("Writing to {}", output.display());
|
||||
let mut output = BufWriter::new(
|
||||
File::create(output)
|
||||
.with_context(|| format!("Failed to create file {}", output.display()))?,
|
||||
);
|
||||
serde_json::to_writer_pretty(&mut output, &report)?;
|
||||
output.flush()?;
|
||||
} else {
|
||||
serde_json::to_writer_pretty(std::io::stdout(), &report)?;
|
||||
}
|
||||
write_output(&report, args.output.as_deref(), output_format)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -220,7 +157,7 @@ fn report_object(
|
||||
) -> Result<Option<ReportUnit>> {
|
||||
object.resolve_paths(project_dir, target_dir, base_dir);
|
||||
match (&object.target_path, &object.base_path) {
|
||||
(None, Some(_)) if object.complete != Some(true) => {
|
||||
(None, Some(_)) if !object.complete().unwrap_or(false) => {
|
||||
warn!("Skipping object without target: {}", object.name());
|
||||
return Ok(None);
|
||||
}
|
||||
@@ -230,54 +167,69 @@ fn report_object(
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
let config = diff::DiffObjConfig { relax_reloc_diffs: true, ..Default::default() };
|
||||
let target = object
|
||||
.target_path
|
||||
.as_ref()
|
||||
.map(|p| obj::read::read(p).with_context(|| format!("Failed to open {}", p.display())))
|
||||
.map(|p| {
|
||||
obj::read::read(p, &config).with_context(|| format!("Failed to open {}", p.display()))
|
||||
})
|
||||
.transpose()?;
|
||||
let base = object
|
||||
.base_path
|
||||
.as_ref()
|
||||
.map(|p| obj::read::read(p).with_context(|| format!("Failed to open {}", p.display())))
|
||||
.map(|p| {
|
||||
obj::read::read(p, &config).with_context(|| format!("Failed to open {}", p.display()))
|
||||
})
|
||||
.transpose()?;
|
||||
let config = diff::DiffObjConfig { relax_reloc_diffs: true, ..Default::default() };
|
||||
let result = diff::diff_objs(&config, target.as_ref(), base.as_ref(), None)?;
|
||||
let mut unit = ReportUnit {
|
||||
name: object.name().to_string(),
|
||||
complete: object.complete,
|
||||
|
||||
let metadata = ReportUnitMetadata {
|
||||
complete: object.complete(),
|
||||
module_name: target
|
||||
.as_ref()
|
||||
.and_then(|o| o.split_meta.as_ref())
|
||||
.and_then(|m| m.module_name.clone()),
|
||||
module_id: target.as_ref().and_then(|o| o.split_meta.as_ref()).and_then(|m| m.module_id),
|
||||
..Default::default()
|
||||
source_path: object.metadata.as_ref().and_then(|m| m.source_path.clone()),
|
||||
progress_categories: object
|
||||
.metadata
|
||||
.as_ref()
|
||||
.and_then(|m| m.progress_categories.clone())
|
||||
.unwrap_or_default(),
|
||||
auto_generated: object.metadata.as_ref().and_then(|m| m.auto_generated),
|
||||
};
|
||||
let obj = target.as_ref().or(base.as_ref()).unwrap();
|
||||
let mut measures = Measures::default();
|
||||
let mut sections = vec![];
|
||||
let mut functions = vec![];
|
||||
|
||||
let obj = target.as_ref().or(base.as_ref()).unwrap();
|
||||
let obj_diff = result.left.as_ref().or(result.right.as_ref()).unwrap();
|
||||
for (section, section_diff) in obj.sections.iter().zip(&obj_diff.sections) {
|
||||
let section_match_percent = section_diff.match_percent.unwrap_or_else(|| {
|
||||
// Support cases where we don't have a target object,
|
||||
// assume complete means 100% match
|
||||
if object.complete == Some(true) {
|
||||
if object.complete().unwrap_or(false) {
|
||||
100.0
|
||||
} else {
|
||||
0.0
|
||||
}
|
||||
});
|
||||
unit.sections.push(ReportItem {
|
||||
sections.push(ReportItem {
|
||||
name: section.name.clone(),
|
||||
demangled_name: None,
|
||||
fuzzy_match_percent: section_match_percent,
|
||||
size: section.size,
|
||||
address: section.virtual_address,
|
||||
metadata: Some(ReportItemMetadata {
|
||||
demangled_name: None,
|
||||
virtual_address: section.virtual_address,
|
||||
}),
|
||||
});
|
||||
|
||||
match section.kind {
|
||||
ObjSectionKind::Data | ObjSectionKind::Bss => {
|
||||
unit.total_data += section.size;
|
||||
measures.total_data += section.size;
|
||||
if section_match_percent == 100.0 {
|
||||
unit.matched_data += section.size;
|
||||
measures.matched_data += section.size;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
@@ -299,155 +251,79 @@ fn report_object(
|
||||
let match_percent = symbol_diff.match_percent.unwrap_or_else(|| {
|
||||
// Support cases where we don't have a target object,
|
||||
// assume complete means 100% match
|
||||
if object.complete == Some(true) {
|
||||
if object.complete().unwrap_or(false) {
|
||||
100.0
|
||||
} else {
|
||||
0.0
|
||||
}
|
||||
});
|
||||
unit.fuzzy_match_percent += match_percent * symbol.size as f32;
|
||||
unit.total_code += symbol.size;
|
||||
measures.fuzzy_match_percent += match_percent * symbol.size as f32;
|
||||
measures.total_code += symbol.size;
|
||||
if match_percent == 100.0 {
|
||||
unit.matched_code += symbol.size;
|
||||
measures.matched_code += symbol.size;
|
||||
}
|
||||
unit.functions.push(ReportItem {
|
||||
functions.push(ReportItem {
|
||||
name: symbol.name.clone(),
|
||||
demangled_name: symbol.demangled_name.clone(),
|
||||
size: symbol.size,
|
||||
fuzzy_match_percent: match_percent,
|
||||
address: symbol.virtual_address,
|
||||
metadata: Some(ReportItemMetadata {
|
||||
demangled_name: symbol.demangled_name.clone(),
|
||||
virtual_address: symbol.virtual_address,
|
||||
}),
|
||||
});
|
||||
if match_percent == 100.0 {
|
||||
unit.matched_functions += 1;
|
||||
measures.matched_functions += 1;
|
||||
}
|
||||
unit.total_functions += 1;
|
||||
measures.total_functions += 1;
|
||||
}
|
||||
}
|
||||
if unit.total_code == 0 {
|
||||
unit.fuzzy_match_percent = 100.0;
|
||||
} else {
|
||||
unit.fuzzy_match_percent /= unit.total_code as f32;
|
||||
}
|
||||
Ok(Some(unit))
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Default, serde::Serialize, serde::Deserialize)]
|
||||
struct Changes {
|
||||
from: ChangeInfo,
|
||||
to: ChangeInfo,
|
||||
units: Vec<ChangeUnit>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Default, PartialEq, serde::Serialize, serde::Deserialize)]
|
||||
struct ChangeInfo {
|
||||
fuzzy_match_percent: f32,
|
||||
total_code: u64,
|
||||
matched_code: u64,
|
||||
matched_code_percent: f32,
|
||||
total_data: u64,
|
||||
matched_data: u64,
|
||||
matched_data_percent: f32,
|
||||
total_functions: u32,
|
||||
matched_functions: u32,
|
||||
matched_functions_percent: f32,
|
||||
}
|
||||
|
||||
impl From<&Report> for ChangeInfo {
|
||||
fn from(report: &Report) -> Self {
|
||||
Self {
|
||||
fuzzy_match_percent: report.fuzzy_match_percent,
|
||||
total_code: report.total_code,
|
||||
matched_code: report.matched_code,
|
||||
matched_code_percent: report.matched_code_percent,
|
||||
total_data: report.total_data,
|
||||
matched_data: report.matched_data,
|
||||
matched_data_percent: report.matched_data_percent,
|
||||
total_functions: report.total_functions,
|
||||
matched_functions: report.matched_functions,
|
||||
matched_functions_percent: report.matched_functions_percent,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<&ReportUnit> for ChangeInfo {
|
||||
fn from(value: &ReportUnit) -> Self {
|
||||
Self {
|
||||
fuzzy_match_percent: value.fuzzy_match_percent,
|
||||
total_code: value.total_code,
|
||||
matched_code: value.matched_code,
|
||||
matched_code_percent: if value.total_code == 0 {
|
||||
100.0
|
||||
} else {
|
||||
value.matched_code as f32 / value.total_code as f32 * 100.0
|
||||
},
|
||||
total_data: value.total_data,
|
||||
matched_data: value.matched_data,
|
||||
matched_data_percent: if value.total_data == 0 {
|
||||
100.0
|
||||
} else {
|
||||
value.matched_data as f32 / value.total_data as f32 * 100.0
|
||||
},
|
||||
total_functions: value.total_functions,
|
||||
matched_functions: value.matched_functions,
|
||||
matched_functions_percent: if value.total_functions == 0 {
|
||||
100.0
|
||||
} else {
|
||||
value.matched_functions as f32 / value.total_functions as f32 * 100.0
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Default, serde::Serialize, serde::Deserialize)]
|
||||
struct ChangeUnit {
|
||||
name: String,
|
||||
from: Option<ChangeInfo>,
|
||||
to: Option<ChangeInfo>,
|
||||
sections: Vec<ChangeItem>,
|
||||
functions: Vec<ChangeItem>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Default, serde::Serialize, serde::Deserialize)]
|
||||
struct ChangeItem {
|
||||
name: String,
|
||||
from: Option<ChangeItemInfo>,
|
||||
to: Option<ChangeItemInfo>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Default, PartialEq, serde::Serialize, serde::Deserialize)]
|
||||
struct ChangeItemInfo {
|
||||
fuzzy_match_percent: f32,
|
||||
size: u64,
|
||||
}
|
||||
|
||||
impl From<&ReportItem> for ChangeItemInfo {
|
||||
fn from(value: &ReportItem) -> Self {
|
||||
Self { fuzzy_match_percent: value.fuzzy_match_percent, size: value.size }
|
||||
if metadata.complete.unwrap_or(false) {
|
||||
measures.complete_code = measures.total_code;
|
||||
measures.complete_data = measures.total_data;
|
||||
}
|
||||
measures.calc_fuzzy_match_percent();
|
||||
measures.calc_matched_percent();
|
||||
Ok(Some(ReportUnit {
|
||||
name: object.name().to_string(),
|
||||
measures: Some(measures),
|
||||
sections,
|
||||
functions,
|
||||
metadata: Some(metadata),
|
||||
}))
|
||||
}
|
||||
|
||||
fn changes(args: ChangesArgs) -> Result<()> {
|
||||
let previous = read_report(&args.previous)?;
|
||||
let current = read_report(&args.current)?;
|
||||
let mut changes = Changes {
|
||||
from: ChangeInfo::from(&previous),
|
||||
to: ChangeInfo::from(¤t),
|
||||
units: vec![],
|
||||
let output_format = OutputFormat::from_option(args.format.as_deref())?;
|
||||
let (previous, current) = if args.previous == Path::new("-") && args.current == Path::new("-") {
|
||||
// Special case for comparing two reports from stdin
|
||||
let mut data = vec![];
|
||||
std::io::stdin().read_to_end(&mut data)?;
|
||||
let input = ChangesInput::decode(data.as_slice())?;
|
||||
(input.from.unwrap(), input.to.unwrap())
|
||||
} else {
|
||||
let previous = read_report(&args.previous)?;
|
||||
let current = read_report(&args.current)?;
|
||||
(previous, current)
|
||||
};
|
||||
let mut changes = Changes { from: previous.measures, to: current.measures, units: vec![] };
|
||||
for prev_unit in &previous.units {
|
||||
let curr_unit = current.units.iter().find(|u| u.name == prev_unit.name);
|
||||
let sections = process_items(prev_unit, curr_unit, |u| &u.sections);
|
||||
let functions = process_items(prev_unit, curr_unit, |u| &u.functions);
|
||||
|
||||
let prev_unit_info = ChangeInfo::from(prev_unit);
|
||||
let curr_unit_info = curr_unit.map(ChangeInfo::from);
|
||||
if !functions.is_empty() || !matches!(&curr_unit_info, Some(v) if v == &prev_unit_info) {
|
||||
let prev_measures = prev_unit.measures;
|
||||
let curr_measures = curr_unit.and_then(|u| u.measures);
|
||||
if !functions.is_empty() || prev_measures != curr_measures {
|
||||
changes.units.push(ChangeUnit {
|
||||
name: prev_unit.name.clone(),
|
||||
from: Some(prev_unit_info),
|
||||
to: curr_unit_info,
|
||||
from: prev_measures,
|
||||
to: curr_measures,
|
||||
sections,
|
||||
functions,
|
||||
metadata: curr_unit
|
||||
.as_ref()
|
||||
.and_then(|u| u.metadata.clone())
|
||||
.or_else(|| prev_unit.metadata.clone()),
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -456,23 +332,14 @@ fn changes(args: ChangesArgs) -> Result<()> {
|
||||
changes.units.push(ChangeUnit {
|
||||
name: curr_unit.name.clone(),
|
||||
from: None,
|
||||
to: Some(ChangeInfo::from(curr_unit)),
|
||||
to: curr_unit.measures,
|
||||
sections: process_new_items(&curr_unit.sections),
|
||||
functions: process_new_items(&curr_unit.functions),
|
||||
metadata: curr_unit.metadata.clone(),
|
||||
});
|
||||
}
|
||||
}
|
||||
if let Some(output) = &args.output {
|
||||
info!("Writing to {}", output.display());
|
||||
let mut output = BufWriter::new(
|
||||
File::create(output)
|
||||
.with_context(|| format!("Failed to create file {}", output.display()))?,
|
||||
);
|
||||
serde_json::to_writer_pretty(&mut output, &changes)?;
|
||||
output.flush()?;
|
||||
} else {
|
||||
serde_json::to_writer_pretty(std::io::stdout(), &changes)?;
|
||||
}
|
||||
write_output(&changes, args.output.as_deref(), output_format)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -495,6 +362,7 @@ fn process_items<F: Fn(&ReportUnit) -> &Vec<ReportItem>>(
|
||||
name: prev_func.name.clone(),
|
||||
from: Some(prev_func_info),
|
||||
to: Some(curr_func_info),
|
||||
metadata: curr_func.as_ref().unwrap().metadata.clone(),
|
||||
});
|
||||
}
|
||||
} else {
|
||||
@@ -502,6 +370,7 @@ fn process_items<F: Fn(&ReportUnit) -> &Vec<ReportItem>>(
|
||||
name: prev_func.name.clone(),
|
||||
from: Some(prev_func_info),
|
||||
to: None,
|
||||
metadata: prev_func.metadata.clone(),
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -511,6 +380,7 @@ fn process_items<F: Fn(&ReportUnit) -> &Vec<ReportItem>>(
|
||||
name: curr_func.name.clone(),
|
||||
from: None,
|
||||
to: Some(ChangeItemInfo::from(curr_func)),
|
||||
metadata: curr_func.metadata.clone(),
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -520,6 +390,7 @@ fn process_items<F: Fn(&ReportUnit) -> &Vec<ReportItem>>(
|
||||
name: prev_func.name.clone(),
|
||||
from: Some(ChangeItemInfo::from(prev_func)),
|
||||
to: None,
|
||||
metadata: prev_func.metadata.clone(),
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -529,35 +400,24 @@ fn process_items<F: Fn(&ReportUnit) -> &Vec<ReportItem>>(
|
||||
fn process_new_items(items: &[ReportItem]) -> Vec<ChangeItem> {
|
||||
items
|
||||
.iter()
|
||||
.map(|f| ChangeItem { name: f.name.clone(), from: None, to: Some(ChangeItemInfo::from(f)) })
|
||||
.map(|item| ChangeItem {
|
||||
name: item.name.clone(),
|
||||
from: None,
|
||||
to: Some(ChangeItemInfo::from(item)),
|
||||
metadata: item.metadata.clone(),
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
fn read_report(path: &Path) -> Result<Report> {
|
||||
serde_json::from_reader(BufReader::new(
|
||||
File::open(path).with_context(|| format!("Failed to open {}", path.display()))?,
|
||||
))
|
||||
.with_context(|| format!("Failed to read report {}", path.display()))
|
||||
}
|
||||
|
||||
fn serialize_hex<S>(x: &Option<u64>, s: S) -> Result<S::Ok, S::Error>
|
||||
where S: serde::Serializer {
|
||||
if let Some(x) = x {
|
||||
s.serialize_str(&format!("{:#x}", x))
|
||||
} else {
|
||||
s.serialize_none()
|
||||
}
|
||||
}
|
||||
|
||||
fn deserialize_hex<'de, D>(d: D) -> Result<Option<u64>, D::Error>
|
||||
where D: serde::Deserializer<'de> {
|
||||
use serde::Deserialize;
|
||||
let s = String::deserialize(d)?;
|
||||
if s.is_empty() {
|
||||
Ok(None)
|
||||
} else if !s.starts_with("0x") {
|
||||
Err(serde::de::Error::custom("expected hex string"))
|
||||
} else {
|
||||
u64::from_str_radix(&s[2..], 16).map(Some).map_err(serde::de::Error::custom)
|
||||
if path == Path::new("-") {
|
||||
let mut data = vec![];
|
||||
std::io::stdin().read_to_end(&mut data)?;
|
||||
return Report::parse(&data).with_context(|| "Failed to load report from stdin");
|
||||
}
|
||||
let file = File::open(path).with_context(|| format!("Failed to open {}", path.display()))?;
|
||||
let mmap = unsafe { memmap2::Mmap::map(&file) }
|
||||
.with_context(|| format!("Failed to map {}", path.display()))?;
|
||||
Report::parse(mmap.as_ref())
|
||||
.with_context(|| format!("Failed to load report {}", path.display()))
|
||||
}
|
||||
|
||||
@@ -54,7 +54,7 @@ impl FromArgValue for LogLevel {
|
||||
}
|
||||
|
||||
#[derive(FromArgs, PartialEq, Debug)]
|
||||
/// Yet another GameCube/Wii decompilation toolkit.
|
||||
/// A local diffing tool for decompilation projects.
|
||||
struct TopLevel {
|
||||
#[argp(subcommand)]
|
||||
command: SubCommand,
|
||||
@@ -96,7 +96,7 @@ fn main() {
|
||||
// Try to enable ANSI support on Windows.
|
||||
let _ = enable_ansi_support();
|
||||
// Disable isatty check for supports-color. (e.g. when used with ninja)
|
||||
env::set_var("IGNORE_IS_TERMINAL", "1");
|
||||
unsafe { env::set_var("IGNORE_IS_TERMINAL", "1") };
|
||||
supports_color::on(Stream::Stdout).is_some_and(|c| c.has_basic)
|
||||
};
|
||||
|
||||
|
||||
@@ -1 +1,2 @@
|
||||
pub mod output;
|
||||
pub mod term;
|
||||
|
||||
84
objdiff-cli/src/util/output.rs
Normal file
84
objdiff-cli/src/util/output.rs
Normal file
@@ -0,0 +1,84 @@
|
||||
use std::{
|
||||
fs::File,
|
||||
io::{BufWriter, Write},
|
||||
ops::DerefMut,
|
||||
path::Path,
|
||||
};
|
||||
|
||||
use anyhow::{bail, Context, Result};
|
||||
use tracing::info;
|
||||
|
||||
#[derive(Debug, Copy, Clone, PartialEq, Eq, Default)]
|
||||
pub enum OutputFormat {
|
||||
#[default]
|
||||
Json,
|
||||
JsonPretty,
|
||||
Proto,
|
||||
}
|
||||
|
||||
impl OutputFormat {
|
||||
pub fn from_str(s: &str) -> Result<Self> {
|
||||
match s.to_ascii_lowercase().as_str() {
|
||||
"json" => Ok(Self::Json),
|
||||
"json-pretty" | "json_pretty" => Ok(Self::JsonPretty),
|
||||
"binpb" | "pb" | "proto" | "protobuf" => Ok(Self::Proto),
|
||||
_ => bail!("Invalid output format: {}", s),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn from_option(s: Option<&str>) -> Result<Self> {
|
||||
match s {
|
||||
Some(s) => Self::from_str(s),
|
||||
None => Ok(Self::default()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn write_output<T>(input: &T, output: Option<&Path>, format: OutputFormat) -> Result<()>
|
||||
where T: serde::Serialize + prost::Message {
|
||||
match output {
|
||||
Some(output) if output != Path::new("-") => {
|
||||
info!("Writing to {}", output.display());
|
||||
let file = File::options()
|
||||
.read(true)
|
||||
.write(true)
|
||||
.create(true)
|
||||
.truncate(true)
|
||||
.open(output)
|
||||
.with_context(|| format!("Failed to create file {}", output.display()))?;
|
||||
match format {
|
||||
OutputFormat::Json => {
|
||||
let mut output = BufWriter::new(file);
|
||||
serde_json::to_writer(&mut output, input)
|
||||
.context("Failed to write output file")?;
|
||||
output.flush().context("Failed to flush output file")?;
|
||||
}
|
||||
OutputFormat::JsonPretty => {
|
||||
let mut output = BufWriter::new(file);
|
||||
serde_json::to_writer_pretty(&mut output, input)
|
||||
.context("Failed to write output file")?;
|
||||
output.flush().context("Failed to flush output file")?;
|
||||
}
|
||||
OutputFormat::Proto => {
|
||||
file.set_len(input.encoded_len() as u64)?;
|
||||
let map = unsafe { memmap2::Mmap::map(&file) }
|
||||
.context("Failed to map output file")?;
|
||||
let mut output = map.make_mut().context("Failed to remap output file")?;
|
||||
input.encode(&mut output.deref_mut()).context("Failed to encode output")?;
|
||||
}
|
||||
}
|
||||
}
|
||||
_ => match format {
|
||||
OutputFormat::Json => {
|
||||
serde_json::to_writer(std::io::stdout(), input)?;
|
||||
}
|
||||
OutputFormat::JsonPretty => {
|
||||
serde_json::to_writer_pretty(std::io::stdout(), input)?;
|
||||
}
|
||||
OutputFormat::Proto => {
|
||||
std::io::stdout().write_all(&input.encode_to_vec())?;
|
||||
}
|
||||
},
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "objdiff-core"
|
||||
version = "2.0.0-alpha.1"
|
||||
version = "2.0.0-beta.6"
|
||||
edition = "2021"
|
||||
rust-version = "1.70"
|
||||
authors = ["Luke Street <luke@street.dev>"]
|
||||
@@ -11,14 +11,20 @@ description = """
|
||||
A local diffing tool for decompilation projects.
|
||||
"""
|
||||
|
||||
[lib]
|
||||
crate-type = ["cdylib", "rlib"]
|
||||
|
||||
[features]
|
||||
all = ["config", "dwarf", "mips", "ppc", "x86"]
|
||||
all = ["config", "dwarf", "mips", "ppc", "x86", "arm", "bindings"]
|
||||
any-arch = [] # Implicit, used to check if any arch is enabled
|
||||
config = ["globset", "semver", "serde_json", "serde_yaml"]
|
||||
dwarf = ["gimli"]
|
||||
mips = ["any-arch", "rabbitizer"]
|
||||
ppc = ["any-arch", "cwdemangle", "ppc750cl"]
|
||||
ppc = ["any-arch", "cwdemangle", "cwextab", "ppc750cl"]
|
||||
x86 = ["any-arch", "cpp_demangle", "iced-x86", "msvc-demangler"]
|
||||
arm = ["any-arch", "cpp_demangle", "unarm", "arm-attr"]
|
||||
bindings = ["serde_json", "prost", "pbjson"]
|
||||
wasm = ["bindings", "console_error_panic_hook", "console_log"]
|
||||
|
||||
[dependencies]
|
||||
anyhow = "1.0.82"
|
||||
@@ -28,9 +34,16 @@ flagset = "0.4.5"
|
||||
log = "0.4.21"
|
||||
memmap2 = "0.9.4"
|
||||
num-traits = "0.2.18"
|
||||
object = { version = "0.35.0", features = ["read_core", "std", "elf", "pe"], default-features = false }
|
||||
object = { version = "0.36.0", features = ["read_core", "std", "elf", "pe"], default-features = false }
|
||||
pbjson = { version = "0.7.0", optional = true }
|
||||
prost = { version = "0.13.1", optional = true }
|
||||
serde = { version = "1", features = ["derive"] }
|
||||
similar = { version = "2.5.0", default-features = false }
|
||||
strum = { version = "0.26.2", features = ["derive"] }
|
||||
wasm-bindgen = "0.2.93"
|
||||
tsify-next = { version = "0.5.4", default-features = false, features = ["js"] }
|
||||
console_log = { version = "1.0.0", optional = true }
|
||||
console_error_panic_hook = { version = "0.1.7", optional = true }
|
||||
|
||||
# config
|
||||
globset = { version = "0.4.14", features = ["serde1"], optional = true }
|
||||
@@ -43,12 +56,21 @@ gimli = { version = "0.29.0", default-features = false, features = ["read-all"],
|
||||
|
||||
# ppc
|
||||
cwdemangle = { version = "1.0.0", optional = true }
|
||||
cwextab = { version = "0.2.3", optional = true }
|
||||
ppc750cl = { git = "https://github.com/encounter/ppc750cl", rev = "6cbd7d888c7082c2c860f66cbb9848d633f753ed", optional = true }
|
||||
|
||||
# mips
|
||||
rabbitizer = { version = "1.10.0", optional = true }
|
||||
rabbitizer = { version = "1.11.0", optional = true }
|
||||
|
||||
# x86
|
||||
cpp_demangle = { version = "0.4.3", optional = true }
|
||||
iced-x86 = { version = "1.21.0", default-features = false, features = ["std", "decoder", "intel", "gas", "masm", "nasm", "exhaustive_enums"], optional = true }
|
||||
msvc-demangler = { version = "0.10.0", optional = true }
|
||||
|
||||
# arm
|
||||
unarm = { version = "1.5.0", optional = true }
|
||||
arm-attr = { version = "0.1.1", optional = true }
|
||||
|
||||
[build-dependencies]
|
||||
prost-build = "0.13.1"
|
||||
pbjson-build = "0.7.0"
|
||||
|
||||
54
objdiff-core/build.rs
Normal file
54
objdiff-core/build.rs
Normal file
@@ -0,0 +1,54 @@
|
||||
use std::path::{Path, PathBuf};
|
||||
|
||||
fn main() {
|
||||
let root = PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("protos");
|
||||
let descriptor_path = root.join("proto_descriptor.bin");
|
||||
println!("cargo:rerun-if-changed={}", descriptor_path.display());
|
||||
let descriptor_mtime = std::fs::metadata(&descriptor_path)
|
||||
.map(|m| m.modified().unwrap())
|
||||
.unwrap_or(std::time::SystemTime::UNIX_EPOCH);
|
||||
let mut run_protoc = false;
|
||||
let proto_files = vec![root.join("diff.proto"), root.join("report.proto")];
|
||||
for proto_file in &proto_files {
|
||||
println!("cargo:rerun-if-changed={}", proto_file.display());
|
||||
let mtime = match std::fs::metadata(proto_file) {
|
||||
Ok(m) => m.modified().unwrap(),
|
||||
Err(e) => panic!("Failed to stat proto file {}: {:?}", proto_file.display(), e),
|
||||
};
|
||||
if mtime > descriptor_mtime {
|
||||
run_protoc = true;
|
||||
}
|
||||
}
|
||||
|
||||
fn prost_config(descriptor_path: &Path, run_protoc: bool) -> prost_build::Config {
|
||||
let mut config = prost_build::Config::new();
|
||||
config.file_descriptor_set_path(descriptor_path);
|
||||
// If our cached descriptor is up-to-date, we don't need to run protoc.
|
||||
// This is helpful so that users don't need to have protoc installed
|
||||
// unless they're updating the protos.
|
||||
if !run_protoc {
|
||||
config.skip_protoc_run();
|
||||
}
|
||||
config
|
||||
}
|
||||
if let Err(e) =
|
||||
prost_config(&descriptor_path, run_protoc).compile_protos(&proto_files, &[root.as_path()])
|
||||
{
|
||||
if e.kind() == std::io::ErrorKind::NotFound && e.to_string().contains("protoc") {
|
||||
eprintln!("protoc not found, skipping protobuf compilation");
|
||||
prost_config(&descriptor_path, false)
|
||||
.compile_protos(&proto_files, &[root.as_path()])
|
||||
.expect("Failed to compile protos");
|
||||
} else {
|
||||
panic!("Failed to compile protos: {e:?}");
|
||||
}
|
||||
}
|
||||
|
||||
let descriptor_set = std::fs::read(descriptor_path).expect("Failed to read descriptor set");
|
||||
pbjson_build::Builder::new()
|
||||
.register_descriptors(&descriptor_set)
|
||||
.expect("Failed to register descriptors")
|
||||
.preserve_proto_field_names()
|
||||
.build(&[".objdiff"])
|
||||
.expect("Failed to build pbjson");
|
||||
}
|
||||
163
objdiff-core/protos/diff.proto
Normal file
163
objdiff-core/protos/diff.proto
Normal file
@@ -0,0 +1,163 @@
|
||||
syntax = "proto3";
|
||||
|
||||
package objdiff.diff;
|
||||
|
||||
// A symbol
|
||||
message Symbol {
|
||||
// Name of the symbol
|
||||
string name = 1;
|
||||
// Demangled name of the symbol
|
||||
optional string demangled_name = 2;
|
||||
// Symbol address
|
||||
uint64 address = 3;
|
||||
// Symbol size
|
||||
uint64 size = 4;
|
||||
// Bitmask of SymbolFlag
|
||||
uint32 flags = 5;
|
||||
}
|
||||
|
||||
// Symbol visibility flags
|
||||
enum SymbolFlag {
|
||||
SYMBOL_NONE = 0;
|
||||
SYMBOL_GLOBAL = 1;
|
||||
SYMBOL_LOCAL = 2;
|
||||
SYMBOL_WEAK = 3;
|
||||
SYMBOL_COMMON = 4;
|
||||
SYMBOL_HIDDEN = 5;
|
||||
}
|
||||
|
||||
// A single parsed instruction
|
||||
message Instruction {
|
||||
// Instruction address
|
||||
uint64 address = 1;
|
||||
// Instruction size
|
||||
uint32 size = 2;
|
||||
// Instruction opcode
|
||||
uint32 opcode = 3;
|
||||
// Instruction mnemonic
|
||||
string mnemonic = 4;
|
||||
// Instruction formatted string
|
||||
string formatted = 5;
|
||||
// Original (unsimplified) instruction string
|
||||
optional string original = 6;
|
||||
// Instruction arguments
|
||||
repeated Argument arguments = 7;
|
||||
// Instruction relocation
|
||||
optional Relocation relocation = 8;
|
||||
// Instruction branch destination
|
||||
optional uint64 branch_dest = 9;
|
||||
// Instruction line number
|
||||
optional uint32 line_number = 10;
|
||||
}
|
||||
|
||||
// An instruction argument
|
||||
message Argument {
|
||||
oneof value {
|
||||
// Plain text
|
||||
string plain_text = 1;
|
||||
// Value
|
||||
ArgumentValue argument = 2;
|
||||
// Relocation
|
||||
ArgumentRelocation relocation = 3;
|
||||
// Branch destination
|
||||
uint64 branch_dest = 4;
|
||||
}
|
||||
}
|
||||
|
||||
// An instruction argument value
|
||||
message ArgumentValue {
|
||||
oneof value {
|
||||
// Signed integer
|
||||
int64 signed = 1;
|
||||
// Unsigned integer
|
||||
uint64 unsigned = 2;
|
||||
// Opaque value
|
||||
string opaque = 3;
|
||||
}
|
||||
}
|
||||
|
||||
// Marker type for relocation arguments
|
||||
message ArgumentRelocation {
|
||||
}
|
||||
|
||||
message Relocation {
|
||||
uint32 type = 1;
|
||||
string type_name = 2;
|
||||
RelocationTarget target = 3;
|
||||
}
|
||||
|
||||
message RelocationTarget {
|
||||
Symbol symbol = 1;
|
||||
int64 addend = 2;
|
||||
}
|
||||
|
||||
message InstructionDiff {
|
||||
DiffKind diff_kind = 1;
|
||||
optional Instruction instruction = 2;
|
||||
optional InstructionBranchFrom branch_from = 3;
|
||||
optional InstructionBranchTo branch_to = 4;
|
||||
repeated ArgumentDiff arg_diff = 5;
|
||||
}
|
||||
|
||||
message ArgumentDiff {
|
||||
optional uint32 diff_index = 1;
|
||||
}
|
||||
|
||||
enum DiffKind {
|
||||
DIFF_NONE = 0;
|
||||
DIFF_REPLACE = 1;
|
||||
DIFF_DELETE = 2;
|
||||
DIFF_INSERT = 3;
|
||||
DIFF_OP_MISMATCH = 4;
|
||||
DIFF_ARG_MISMATCH = 5;
|
||||
}
|
||||
|
||||
message InstructionBranchFrom {
|
||||
repeated uint32 instruction_index = 1;
|
||||
uint32 branch_index = 2;
|
||||
}
|
||||
|
||||
message InstructionBranchTo {
|
||||
uint32 instruction_index = 1;
|
||||
uint32 branch_index = 2;
|
||||
}
|
||||
|
||||
message FunctionDiff {
|
||||
Symbol symbol = 1;
|
||||
repeated InstructionDiff instructions = 2;
|
||||
optional float match_percent = 3;
|
||||
}
|
||||
|
||||
message DataDiff {
|
||||
DiffKind kind = 1;
|
||||
bytes data = 2;
|
||||
// May be larger than data
|
||||
uint64 size = 3;
|
||||
}
|
||||
|
||||
message SectionDiff {
|
||||
string name = 1;
|
||||
SectionKind kind = 2;
|
||||
uint64 size = 3;
|
||||
uint64 address = 4;
|
||||
repeated FunctionDiff functions = 5;
|
||||
repeated DataDiff data = 6;
|
||||
optional float match_percent = 7;
|
||||
}
|
||||
|
||||
enum SectionKind {
|
||||
SECTION_UNKNOWN = 0;
|
||||
SECTION_TEXT = 1;
|
||||
SECTION_DATA = 2;
|
||||
SECTION_BSS = 3;
|
||||
SECTION_COMMON = 4;
|
||||
}
|
||||
|
||||
message ObjectDiff {
|
||||
repeated SectionDiff sections = 1;
|
||||
}
|
||||
|
||||
message DiffResult {
|
||||
optional ObjectDiff left = 1;
|
||||
optional ObjectDiff right = 2;
|
||||
}
|
||||
BIN
objdiff-core/protos/proto_descriptor.bin
Normal file
BIN
objdiff-core/protos/proto_descriptor.bin
Normal file
Binary file not shown.
160
objdiff-core/protos/report.proto
Normal file
160
objdiff-core/protos/report.proto
Normal file
@@ -0,0 +1,160 @@
|
||||
syntax = "proto3";
|
||||
|
||||
package objdiff.report;
|
||||
|
||||
// Progress info for a report or unit
|
||||
message Measures {
|
||||
// Overall match percent, including partially matched functions and data
|
||||
float fuzzy_match_percent = 1;
|
||||
// Total size of code in bytes
|
||||
uint64 total_code = 2;
|
||||
// Fully matched code size in bytes
|
||||
uint64 matched_code = 3;
|
||||
// Fully matched code percent
|
||||
float matched_code_percent = 4;
|
||||
// Total size of data in bytes
|
||||
uint64 total_data = 5;
|
||||
// Fully matched data size in bytes
|
||||
uint64 matched_data = 6;
|
||||
// Fully matched data percent
|
||||
float matched_data_percent = 7;
|
||||
// Total number of functions
|
||||
uint32 total_functions = 8;
|
||||
// Fully matched functions
|
||||
uint32 matched_functions = 9;
|
||||
// Fully matched functions percent
|
||||
float matched_functions_percent = 10;
|
||||
// Completed (or "linked") code size in bytes
|
||||
uint64 complete_code = 11;
|
||||
// Completed (or "linked") code percent
|
||||
float complete_code_percent = 12;
|
||||
// Completed (or "linked") data size in bytes
|
||||
uint64 complete_data = 13;
|
||||
// Completed (or "linked") data percent
|
||||
float complete_data_percent = 14;
|
||||
}
|
||||
|
||||
// Project progress report
|
||||
message Report {
|
||||
// Overall progress info
|
||||
Measures measures = 1;
|
||||
// Units within this report
|
||||
repeated ReportUnit units = 2;
|
||||
// Report version
|
||||
uint32 version = 3;
|
||||
// Progress categories
|
||||
repeated ReportCategory categories = 4;
|
||||
}
|
||||
|
||||
message ReportCategory {
|
||||
// The ID of the category
|
||||
string id = 1;
|
||||
// The name of the category
|
||||
string name = 2;
|
||||
// Progress info for this category
|
||||
Measures measures = 3;
|
||||
}
|
||||
|
||||
// A unit of the report (usually a translation unit)
|
||||
message ReportUnit {
|
||||
// The name of the unit
|
||||
string name = 1;
|
||||
// Progress info for this unit
|
||||
Measures measures = 2;
|
||||
// Sections within this unit
|
||||
repeated ReportItem sections = 3;
|
||||
// Functions within this unit
|
||||
repeated ReportItem functions = 4;
|
||||
// Extra metadata for this unit
|
||||
optional ReportUnitMetadata metadata = 5;
|
||||
}
|
||||
|
||||
// Extra metadata for a unit
|
||||
message ReportUnitMetadata {
|
||||
// Whether this unit is marked as complete (or "linked")
|
||||
optional bool complete = 1;
|
||||
// The name of the module this unit belongs to
|
||||
optional string module_name = 2;
|
||||
// The ID of the module this unit belongs to
|
||||
optional uint32 module_id = 3;
|
||||
// The path to the source file of this unit
|
||||
optional string source_path = 4;
|
||||
// Progress categories for this unit
|
||||
repeated string progress_categories = 5;
|
||||
// Whether this unit is automatically generated (not user-provided)
|
||||
optional bool auto_generated = 6;
|
||||
}
|
||||
|
||||
// A section or function within a unit
|
||||
message ReportItem {
|
||||
// The name of the item
|
||||
string name = 1;
|
||||
// The size of the item in bytes
|
||||
uint64 size = 2;
|
||||
// The overall match percent for this item
|
||||
float fuzzy_match_percent = 3;
|
||||
// Extra metadata for this item
|
||||
optional ReportItemMetadata metadata = 4;
|
||||
}
|
||||
|
||||
// Extra metadata for an item
|
||||
message ReportItemMetadata {
|
||||
// The demangled name of the function
|
||||
optional string demangled_name = 1;
|
||||
// The virtual address of the function or section
|
||||
optional uint64 virtual_address = 2;
|
||||
}
|
||||
|
||||
// A pair of reports to compare and generate changes
|
||||
message ChangesInput {
|
||||
// The previous report
|
||||
Report from = 1;
|
||||
// The current report
|
||||
Report to = 2;
|
||||
}
|
||||
|
||||
// Changes between two reports
|
||||
message Changes {
|
||||
// The progress info for the previous report
|
||||
Measures from = 1;
|
||||
// The progress info for the current report
|
||||
Measures to = 2;
|
||||
// Units that changed
|
||||
repeated ChangeUnit units = 3;
|
||||
}
|
||||
|
||||
// A changed unit
|
||||
message ChangeUnit {
|
||||
// The name of the unit
|
||||
string name = 1;
|
||||
// The previous progress info (omitted if new)
|
||||
optional Measures from = 2;
|
||||
// The current progress info (omitted if removed)
|
||||
optional Measures to = 3;
|
||||
// Sections that changed
|
||||
repeated ChangeItem sections = 4;
|
||||
// Functions that changed
|
||||
repeated ChangeItem functions = 5;
|
||||
// Extra metadata for this unit
|
||||
optional ReportUnitMetadata metadata = 6;
|
||||
}
|
||||
|
||||
// A changed section or function
|
||||
message ChangeItem {
|
||||
// The name of the item
|
||||
string name = 1;
|
||||
// The previous progress info (omitted if new)
|
||||
optional ChangeItemInfo from = 2;
|
||||
// The current progress info (omitted if removed)
|
||||
optional ChangeItemInfo to = 3;
|
||||
// Extra metadata for this item
|
||||
optional ReportItemMetadata metadata = 4;
|
||||
}
|
||||
|
||||
// Progress info for a section or function
|
||||
message ChangeItemInfo {
|
||||
// The overall match percent for this item
|
||||
float fuzzy_match_percent = 1;
|
||||
// The size of the item in bytes
|
||||
uint64 size = 2;
|
||||
}
|
||||
443
objdiff-core/src/arch/arm.rs
Normal file
443
objdiff-core/src/arch/arm.rs
Normal file
@@ -0,0 +1,443 @@
|
||||
use std::{
|
||||
borrow::Cow,
|
||||
collections::{BTreeMap, HashMap},
|
||||
};
|
||||
|
||||
use anyhow::{bail, Result};
|
||||
use arm_attr::{enums::CpuArch, tag::Tag, BuildAttrs};
|
||||
use object::{
|
||||
elf::{self, SHT_ARM_ATTRIBUTES},
|
||||
Endian, File, Object, ObjectSection, ObjectSymbol, Relocation, RelocationFlags, SectionIndex,
|
||||
SectionKind, Symbol, SymbolKind,
|
||||
};
|
||||
use unarm::{
|
||||
args::{Argument, OffsetImm, OffsetReg, Register},
|
||||
parse::{ArmVersion, ParseMode, Parser},
|
||||
DisplayOptions, ParseFlags, ParsedIns, RegNames,
|
||||
};
|
||||
|
||||
use crate::{
|
||||
arch::{ObjArch, ProcessCodeResult},
|
||||
diff::{ArmArchVersion, ArmR9Usage, DiffObjConfig},
|
||||
obj::{ObjIns, ObjInsArg, ObjInsArgValue, ObjReloc, ObjSection},
|
||||
};
|
||||
|
||||
pub struct ObjArchArm {
|
||||
/// Maps section index, to list of disasm modes (arm, thumb or data) sorted by address
|
||||
disasm_modes: HashMap<SectionIndex, Vec<DisasmMode>>,
|
||||
detected_version: Option<ArmVersion>,
|
||||
endianness: object::Endianness,
|
||||
}
|
||||
|
||||
impl ObjArchArm {
|
||||
pub fn new(file: &File) -> Result<Self> {
|
||||
let endianness = file.endianness();
|
||||
match file {
|
||||
File::Elf32(_) => {
|
||||
let disasm_modes = Self::elf_get_mapping_symbols(file);
|
||||
let detected_version = Self::elf_detect_arm_version(file)?;
|
||||
Ok(Self { disasm_modes, detected_version, endianness })
|
||||
}
|
||||
_ => bail!("Unsupported file format {:?}", file.format()),
|
||||
}
|
||||
}
|
||||
|
||||
fn elf_detect_arm_version(file: &File) -> Result<Option<ArmVersion>> {
|
||||
// Check ARM attributes
|
||||
if let Some(arm_attrs) = file.sections().find(|s| {
|
||||
s.kind() == SectionKind::Elf(SHT_ARM_ATTRIBUTES) && s.name() == Ok(".ARM.attributes")
|
||||
}) {
|
||||
let attr_data = arm_attrs.uncompressed_data()?;
|
||||
let build_attrs = BuildAttrs::new(&attr_data, match file.endianness() {
|
||||
object::Endianness::Little => arm_attr::Endian::Little,
|
||||
object::Endianness::Big => arm_attr::Endian::Big,
|
||||
})?;
|
||||
for subsection in build_attrs.subsections() {
|
||||
let subsection = subsection?;
|
||||
if !subsection.is_aeabi() {
|
||||
continue;
|
||||
}
|
||||
// Only checking first CpuArch tag. Others may exist, but that's very unlikely.
|
||||
let cpu_arch = subsection.into_public_tag_iter()?.find_map(|(_, tag)| {
|
||||
if let Tag::CpuArch(cpu_arch) = tag {
|
||||
Some(cpu_arch)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
});
|
||||
match cpu_arch {
|
||||
Some(CpuArch::V4T) => return Ok(Some(ArmVersion::V4T)),
|
||||
Some(CpuArch::V5TE) => return Ok(Some(ArmVersion::V5Te)),
|
||||
Some(CpuArch::V6K) => return Ok(Some(ArmVersion::V6K)),
|
||||
Some(arch) => bail!("ARM arch {} not supported", arch),
|
||||
None => {}
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
Ok(None)
|
||||
}
|
||||
|
||||
fn elf_get_mapping_symbols(file: &File) -> HashMap<SectionIndex, Vec<DisasmMode>> {
|
||||
file.sections()
|
||||
.filter(|s| s.kind() == SectionKind::Text)
|
||||
.map(|s| {
|
||||
let index = s.index();
|
||||
let mut mapping_symbols: Vec<_> = file
|
||||
.symbols()
|
||||
.filter(|s| s.section_index().map(|i| i == index).unwrap_or(false))
|
||||
.filter_map(|s| DisasmMode::from_symbol(&s))
|
||||
.collect();
|
||||
mapping_symbols.sort_unstable_by_key(|x| x.address);
|
||||
(s.index(), mapping_symbols)
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
}
|
||||
|
||||
impl ObjArch for ObjArchArm {
|
||||
fn symbol_address(&self, symbol: &Symbol) -> u64 {
|
||||
let address = symbol.address();
|
||||
if symbol.kind() == SymbolKind::Text {
|
||||
address & !1
|
||||
} else {
|
||||
address
|
||||
}
|
||||
}
|
||||
|
||||
fn process_code(
|
||||
&self,
|
||||
address: u64,
|
||||
code: &[u8],
|
||||
section_index: usize,
|
||||
relocations: &[ObjReloc],
|
||||
line_info: &BTreeMap<u64, u32>,
|
||||
config: &DiffObjConfig,
|
||||
) -> Result<ProcessCodeResult> {
|
||||
let start_addr = address as u32;
|
||||
let end_addr = start_addr + code.len() as u32;
|
||||
|
||||
// Mapping symbols decide what kind of data comes after it. $a for ARM code, $t for Thumb code and $d for data.
|
||||
let fallback_mappings = [DisasmMode { address: start_addr, mapping: ParseMode::Arm }];
|
||||
let mapping_symbols = self
|
||||
.disasm_modes
|
||||
.get(&SectionIndex(section_index))
|
||||
.map(|x| x.as_slice())
|
||||
.unwrap_or(&fallback_mappings);
|
||||
let first_mapping_idx =
|
||||
match mapping_symbols.binary_search_by_key(&start_addr, |x| x.address) {
|
||||
Ok(idx) => idx,
|
||||
Err(idx) => idx - 1,
|
||||
};
|
||||
let first_mapping = mapping_symbols[first_mapping_idx].mapping;
|
||||
|
||||
let mut mappings_iter =
|
||||
mapping_symbols.iter().skip(first_mapping_idx + 1).take_while(|x| x.address < end_addr);
|
||||
let mut next_mapping = mappings_iter.next();
|
||||
|
||||
let ins_count = code.len() / first_mapping.instruction_size(start_addr);
|
||||
let mut ops = Vec::<u16>::with_capacity(ins_count);
|
||||
let mut insts = Vec::<ObjIns>::with_capacity(ins_count);
|
||||
|
||||
let version = match config.arm_arch_version {
|
||||
ArmArchVersion::Auto => self.detected_version.unwrap_or(ArmVersion::V5Te),
|
||||
ArmArchVersion::V4T => ArmVersion::V4T,
|
||||
ArmArchVersion::V5TE => ArmVersion::V5Te,
|
||||
ArmArchVersion::V6K => ArmVersion::V6K,
|
||||
};
|
||||
let endian = match self.endianness {
|
||||
object::Endianness::Little => unarm::Endian::Little,
|
||||
object::Endianness::Big => unarm::Endian::Big,
|
||||
};
|
||||
|
||||
let parse_flags = ParseFlags { ual: config.arm_unified_syntax, version };
|
||||
|
||||
let mut parser = Parser::new(first_mapping, start_addr, endian, parse_flags, code);
|
||||
|
||||
let display_options = DisplayOptions {
|
||||
reg_names: RegNames {
|
||||
av_registers: config.arm_av_registers,
|
||||
r9_use: match config.arm_r9_usage {
|
||||
ArmR9Usage::GeneralPurpose => unarm::R9Use::GeneralPurpose,
|
||||
ArmR9Usage::Sb => unarm::R9Use::Pid,
|
||||
ArmR9Usage::Tr => unarm::R9Use::Tls,
|
||||
},
|
||||
explicit_stack_limit: config.arm_sl_usage,
|
||||
frame_pointer: config.arm_fp_usage,
|
||||
ip: config.arm_ip_usage,
|
||||
},
|
||||
};
|
||||
|
||||
while let Some((address, ins, parsed_ins)) = parser.next() {
|
||||
if let Some(next) = next_mapping {
|
||||
let next_address = parser.address;
|
||||
if next_address >= next.address {
|
||||
// Change mapping
|
||||
parser.mode = next.mapping;
|
||||
next_mapping = mappings_iter.next();
|
||||
}
|
||||
}
|
||||
let line = line_info.range(..=address as u64).last().map(|(_, &b)| b);
|
||||
|
||||
let reloc = relocations.iter().find(|r| (r.address as u32 & !1) == address).cloned();
|
||||
|
||||
let mut reloc_arg = None;
|
||||
if let Some(reloc) = &reloc {
|
||||
match reloc.flags {
|
||||
// Calls
|
||||
RelocationFlags::Elf { r_type: elf::R_ARM_THM_XPC22 }
|
||||
| RelocationFlags::Elf { r_type: elf::R_ARM_THM_PC22 }
|
||||
| RelocationFlags::Elf { r_type: elf::R_ARM_PC24 }
|
||||
| RelocationFlags::Elf { r_type: elf::R_ARM_XPC25 }
|
||||
| RelocationFlags::Elf { r_type: elf::R_ARM_CALL } => {
|
||||
reloc_arg = parsed_ins
|
||||
.args
|
||||
.iter()
|
||||
.rposition(|a| matches!(a, Argument::BranchDest(_)));
|
||||
}
|
||||
// Data
|
||||
RelocationFlags::Elf { r_type: elf::R_ARM_ABS32 } => {
|
||||
reloc_arg =
|
||||
parsed_ins.args.iter().rposition(|a| matches!(a, Argument::UImm(_)));
|
||||
}
|
||||
_ => (),
|
||||
}
|
||||
};
|
||||
|
||||
let (args, branch_dest) = if reloc.is_some() && parser.mode == ParseMode::Data {
|
||||
(vec![ObjInsArg::Reloc], None)
|
||||
} else {
|
||||
push_args(&parsed_ins, config, reloc_arg, address, display_options)?
|
||||
};
|
||||
|
||||
ops.push(ins.opcode_id());
|
||||
insts.push(ObjIns {
|
||||
address: address as u64,
|
||||
size: (parser.address - address) as u8,
|
||||
op: ins.opcode_id(),
|
||||
mnemonic: parsed_ins.mnemonic.to_string(),
|
||||
args,
|
||||
reloc,
|
||||
branch_dest,
|
||||
line,
|
||||
formatted: parsed_ins.display(display_options).to_string(),
|
||||
orig: None,
|
||||
});
|
||||
}
|
||||
|
||||
Ok(ProcessCodeResult { ops, insts })
|
||||
}
|
||||
|
||||
fn implcit_addend(
|
||||
&self,
|
||||
_file: &File<'_>,
|
||||
section: &ObjSection,
|
||||
address: u64,
|
||||
reloc: &Relocation,
|
||||
) -> anyhow::Result<i64> {
|
||||
let address = address as usize;
|
||||
Ok(match reloc.flags() {
|
||||
// ARM calls
|
||||
RelocationFlags::Elf { r_type: elf::R_ARM_PC24 }
|
||||
| RelocationFlags::Elf { r_type: elf::R_ARM_XPC25 }
|
||||
| RelocationFlags::Elf { r_type: elf::R_ARM_CALL } => {
|
||||
let data = section.data[address..address + 4].try_into()?;
|
||||
let addend = self.endianness.read_i32_bytes(data);
|
||||
let imm24 = addend & 0xffffff;
|
||||
(imm24 << 2) << 8 >> 8
|
||||
}
|
||||
|
||||
// Thumb calls
|
||||
RelocationFlags::Elf { r_type: elf::R_ARM_THM_PC22 }
|
||||
| RelocationFlags::Elf { r_type: elf::R_ARM_THM_XPC22 } => {
|
||||
let data = section.data[address..address + 2].try_into()?;
|
||||
let high = self.endianness.read_i16_bytes(data) as i32;
|
||||
let data = section.data[address + 2..address + 4].try_into()?;
|
||||
let low = self.endianness.read_i16_bytes(data) as i32;
|
||||
|
||||
let imm22 = ((high & 0x7ff) << 11) | (low & 0x7ff);
|
||||
(imm22 << 1) << 9 >> 9
|
||||
}
|
||||
|
||||
// Data
|
||||
RelocationFlags::Elf { r_type: elf::R_ARM_ABS32 } => {
|
||||
let data = section.data[address..address + 4].try_into()?;
|
||||
self.endianness.read_i32_bytes(data)
|
||||
}
|
||||
|
||||
flags => bail!("Unsupported ARM implicit relocation {flags:?}"),
|
||||
} as i64)
|
||||
}
|
||||
|
||||
fn demangle(&self, name: &str) -> Option<String> {
|
||||
cpp_demangle::Symbol::new(name)
|
||||
.ok()
|
||||
.and_then(|s| s.demangle(&cpp_demangle::DemangleOptions::default()).ok())
|
||||
}
|
||||
|
||||
fn display_reloc(&self, flags: RelocationFlags) -> Cow<'static, str> {
|
||||
Cow::Owned(format!("<{flags:?}>"))
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug)]
|
||||
struct DisasmMode {
|
||||
address: u32,
|
||||
mapping: ParseMode,
|
||||
}
|
||||
|
||||
impl DisasmMode {
|
||||
fn from_symbol<'a>(sym: &Symbol<'a, '_, &'a [u8]>) -> Option<Self> {
|
||||
if let Ok(name) = sym.name() {
|
||||
ParseMode::from_mapping_symbol(name)
|
||||
.map(|mapping| DisasmMode { address: sym.address() as u32, mapping })
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn push_args(
|
||||
parsed_ins: &ParsedIns,
|
||||
config: &DiffObjConfig,
|
||||
reloc_arg: Option<usize>,
|
||||
cur_addr: u32,
|
||||
display_options: DisplayOptions,
|
||||
) -> Result<(Vec<ObjInsArg>, Option<u64>)> {
|
||||
let mut args = vec![];
|
||||
let mut branch_dest = None;
|
||||
let mut writeback = false;
|
||||
let mut deref = false;
|
||||
for (i, arg) in parsed_ins.args_iter().enumerate() {
|
||||
// Emit punctuation before separator
|
||||
if deref {
|
||||
match arg {
|
||||
Argument::OffsetImm(OffsetImm { post_indexed: true, value: _ })
|
||||
| Argument::OffsetReg(OffsetReg { add: _, post_indexed: true, reg: _ })
|
||||
| Argument::CoOption(_) => {
|
||||
deref = false;
|
||||
args.push(ObjInsArg::PlainText("]".into()));
|
||||
if writeback {
|
||||
writeback = false;
|
||||
args.push(ObjInsArg::Arg(ObjInsArgValue::Opaque("!".into())));
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
if i > 0 {
|
||||
args.push(ObjInsArg::PlainText(config.separator().into()));
|
||||
}
|
||||
|
||||
if reloc_arg == Some(i) {
|
||||
args.push(ObjInsArg::Reloc);
|
||||
} else {
|
||||
match arg {
|
||||
Argument::None => {}
|
||||
Argument::Reg(reg) => {
|
||||
if reg.deref {
|
||||
deref = true;
|
||||
args.push(ObjInsArg::PlainText("[".into()));
|
||||
}
|
||||
args.push(ObjInsArg::Arg(ObjInsArgValue::Opaque(
|
||||
reg.reg.display(display_options.reg_names).to_string().into(),
|
||||
)));
|
||||
if reg.writeback {
|
||||
if reg.deref {
|
||||
writeback = true;
|
||||
} else {
|
||||
args.push(ObjInsArg::Arg(ObjInsArgValue::Opaque("!".into())));
|
||||
}
|
||||
}
|
||||
}
|
||||
Argument::RegList(reg_list) => {
|
||||
args.push(ObjInsArg::PlainText("{".into()));
|
||||
let mut first = true;
|
||||
for i in 0..16 {
|
||||
if (reg_list.regs & (1 << i)) != 0 {
|
||||
if !first {
|
||||
args.push(ObjInsArg::PlainText(config.separator().into()));
|
||||
}
|
||||
args.push(ObjInsArg::Arg(ObjInsArgValue::Opaque(
|
||||
Register::parse(i)
|
||||
.display(display_options.reg_names)
|
||||
.to_string()
|
||||
.into(),
|
||||
)));
|
||||
first = false;
|
||||
}
|
||||
}
|
||||
args.push(ObjInsArg::PlainText("}".into()));
|
||||
if reg_list.user_mode {
|
||||
args.push(ObjInsArg::Arg(ObjInsArgValue::Opaque("^".to_string().into())));
|
||||
}
|
||||
}
|
||||
Argument::UImm(value) | Argument::CoOpcode(value) | Argument::SatImm(value) => {
|
||||
args.push(ObjInsArg::PlainText("#".into()));
|
||||
args.push(ObjInsArg::Arg(ObjInsArgValue::Unsigned(*value as u64)));
|
||||
}
|
||||
Argument::SImm(value)
|
||||
| Argument::OffsetImm(OffsetImm { post_indexed: _, value }) => {
|
||||
args.push(ObjInsArg::PlainText("#".into()));
|
||||
args.push(ObjInsArg::Arg(ObjInsArgValue::Signed(*value as i64)));
|
||||
}
|
||||
Argument::BranchDest(value) => {
|
||||
let dest = cur_addr.wrapping_add_signed(*value) as u64;
|
||||
args.push(ObjInsArg::BranchDest(dest));
|
||||
branch_dest = Some(dest);
|
||||
}
|
||||
Argument::CoOption(value) => {
|
||||
args.push(ObjInsArg::PlainText("{".into()));
|
||||
args.push(ObjInsArg::Arg(ObjInsArgValue::Unsigned(*value as u64)));
|
||||
args.push(ObjInsArg::PlainText("}".into()));
|
||||
}
|
||||
Argument::CoprocNum(value) => {
|
||||
args.push(ObjInsArg::Arg(ObjInsArgValue::Opaque(format!("p{}", value).into())));
|
||||
}
|
||||
Argument::ShiftImm(shift) => {
|
||||
args.push(ObjInsArg::Arg(ObjInsArgValue::Opaque(shift.op.to_string().into())));
|
||||
args.push(ObjInsArg::PlainText(" #".into()));
|
||||
args.push(ObjInsArg::Arg(ObjInsArgValue::Unsigned(shift.imm as u64)));
|
||||
}
|
||||
Argument::ShiftReg(shift) => {
|
||||
args.push(ObjInsArg::Arg(ObjInsArgValue::Opaque(shift.op.to_string().into())));
|
||||
args.push(ObjInsArg::PlainText(" ".into()));
|
||||
args.push(ObjInsArg::Arg(ObjInsArgValue::Opaque(
|
||||
shift.reg.display(display_options.reg_names).to_string().into(),
|
||||
)));
|
||||
}
|
||||
Argument::OffsetReg(offset) => {
|
||||
if !offset.add {
|
||||
args.push(ObjInsArg::PlainText("-".into()));
|
||||
}
|
||||
args.push(ObjInsArg::Arg(ObjInsArgValue::Opaque(
|
||||
offset.reg.display(display_options.reg_names).to_string().into(),
|
||||
)));
|
||||
}
|
||||
Argument::CpsrMode(mode) => {
|
||||
args.push(ObjInsArg::PlainText("#".into()));
|
||||
args.push(ObjInsArg::Arg(ObjInsArgValue::Unsigned(mode.mode as u64)));
|
||||
if mode.writeback {
|
||||
args.push(ObjInsArg::Arg(ObjInsArgValue::Opaque("!".into())));
|
||||
}
|
||||
}
|
||||
Argument::CoReg(_)
|
||||
| Argument::StatusReg(_)
|
||||
| Argument::StatusMask(_)
|
||||
| Argument::Shift(_)
|
||||
| Argument::CpsrFlags(_)
|
||||
| Argument::Endian(_) => args.push(ObjInsArg::Arg(ObjInsArgValue::Opaque(
|
||||
arg.display(display_options, None).to_string().into(),
|
||||
))),
|
||||
}
|
||||
}
|
||||
}
|
||||
if deref {
|
||||
args.push(ObjInsArg::PlainText("]".into()));
|
||||
if writeback {
|
||||
args.push(ObjInsArg::Arg(ObjInsArgValue::Opaque("!".into())));
|
||||
}
|
||||
}
|
||||
Ok((args, branch_dest))
|
||||
}
|
||||
@@ -1,54 +1,119 @@
|
||||
use std::borrow::Cow;
|
||||
use std::{borrow::Cow, collections::BTreeMap, sync::Mutex};
|
||||
|
||||
use anyhow::{anyhow, bail, Result};
|
||||
use object::{elf, Endian, Endianness, File, Object, Relocation, RelocationFlags};
|
||||
use object::{
|
||||
elf, Endian, Endianness, File, FileFlags, Object, ObjectSection, ObjectSymbol, Relocation,
|
||||
RelocationFlags, RelocationTarget,
|
||||
};
|
||||
use rabbitizer::{config, Abi, InstrCategory, Instruction, OperandType};
|
||||
|
||||
use crate::{
|
||||
arch::{ObjArch, ProcessCodeResult},
|
||||
diff::DiffObjConfig,
|
||||
obj::{ObjInfo, ObjIns, ObjInsArg, ObjInsArgValue, ObjReloc, ObjSection, SymbolRef},
|
||||
diff::{DiffObjConfig, MipsAbi, MipsInstrCategory},
|
||||
obj::{ObjIns, ObjInsArg, ObjInsArgValue, ObjReloc, ObjSection},
|
||||
};
|
||||
|
||||
fn configure_rabbitizer() {
|
||||
static RABBITIZER_MUTEX: Mutex<()> = Mutex::new(());
|
||||
|
||||
fn configure_rabbitizer(abi: Abi) {
|
||||
unsafe {
|
||||
config::RabbitizerConfig_Cfg.reg_names.fpr_abi_names = Abi::O32;
|
||||
config::RabbitizerConfig_Cfg.reg_names.fpr_abi_names = abi;
|
||||
}
|
||||
}
|
||||
|
||||
pub struct ObjArchMips {
|
||||
pub endianness: Endianness,
|
||||
pub abi: Abi,
|
||||
pub instr_category: InstrCategory,
|
||||
pub ri_gp_value: i32,
|
||||
}
|
||||
|
||||
const EF_MIPS_ABI: u32 = 0x0000F000;
|
||||
const EF_MIPS_MACH: u32 = 0x00FF0000;
|
||||
|
||||
const EF_MIPS_MACH_ALLEGREX: u32 = 0x00840000;
|
||||
const EF_MIPS_MACH_5900: u32 = 0x00920000;
|
||||
|
||||
const R_MIPS15_S3: u32 = 119;
|
||||
|
||||
impl ObjArchMips {
|
||||
pub fn new(object: &File) -> Result<Self> {
|
||||
configure_rabbitizer();
|
||||
Ok(Self { endianness: object.endianness() })
|
||||
let mut abi = Abi::NUMERIC;
|
||||
let mut instr_category = InstrCategory::CPU;
|
||||
match object.flags() {
|
||||
FileFlags::None => {}
|
||||
FileFlags::Elf { e_flags, .. } => {
|
||||
abi = match e_flags & EF_MIPS_ABI {
|
||||
elf::EF_MIPS_ABI_O32 | elf::EF_MIPS_ABI_O64 => Abi::O32,
|
||||
elf::EF_MIPS_ABI_EABI32 | elf::EF_MIPS_ABI_EABI64 => Abi::N32,
|
||||
_ => {
|
||||
if e_flags & elf::EF_MIPS_ABI2 != 0 {
|
||||
Abi::N32
|
||||
} else {
|
||||
Abi::NUMERIC
|
||||
}
|
||||
}
|
||||
};
|
||||
instr_category = match e_flags & EF_MIPS_MACH {
|
||||
EF_MIPS_MACH_ALLEGREX => InstrCategory::R4000ALLEGREX,
|
||||
EF_MIPS_MACH_5900 => InstrCategory::R5900,
|
||||
_ => InstrCategory::CPU,
|
||||
};
|
||||
}
|
||||
_ => bail!("Unsupported MIPS file flags"),
|
||||
}
|
||||
|
||||
// Parse the ri_gp_value stored in .reginfo to be able to correctly
|
||||
// calculate R_MIPS_GPREL16 relocations later. The value is stored
|
||||
// 0x14 bytes into .reginfo (on 32 bit platforms)
|
||||
let ri_gp_value = object
|
||||
.section_by_name(".reginfo")
|
||||
.and_then(|section| section.data().ok())
|
||||
.and_then(|data| data.get(0x14..0x18))
|
||||
.and_then(|s| s.try_into().ok())
|
||||
.map(|bytes| object.endianness().read_i32_bytes(bytes))
|
||||
.unwrap_or(0);
|
||||
|
||||
Ok(Self { endianness: object.endianness(), abi, instr_category, ri_gp_value })
|
||||
}
|
||||
}
|
||||
|
||||
impl ObjArch for ObjArchMips {
|
||||
fn process_code(
|
||||
&self,
|
||||
obj: &ObjInfo,
|
||||
symbol_ref: SymbolRef,
|
||||
address: u64,
|
||||
code: &[u8],
|
||||
_section_index: usize,
|
||||
relocations: &[ObjReloc],
|
||||
line_info: &BTreeMap<u64, u32>,
|
||||
config: &DiffObjConfig,
|
||||
) -> Result<ProcessCodeResult> {
|
||||
let (section, symbol) = obj.section_symbol(symbol_ref);
|
||||
let section = section.ok_or_else(|| anyhow!("Code symbol section not found"))?;
|
||||
let code = §ion.data
|
||||
[symbol.section_address as usize..(symbol.section_address + symbol.size) as usize];
|
||||
let _guard = RABBITIZER_MUTEX.lock().map_err(|e| anyhow!("Failed to lock mutex: {e}"))?;
|
||||
configure_rabbitizer(match config.mips_abi {
|
||||
MipsAbi::Auto => self.abi,
|
||||
MipsAbi::O32 => Abi::O32,
|
||||
MipsAbi::N32 => Abi::N32,
|
||||
MipsAbi::N64 => Abi::N64,
|
||||
});
|
||||
let instr_category = match config.mips_instr_category {
|
||||
MipsInstrCategory::Auto => self.instr_category,
|
||||
MipsInstrCategory::Cpu => InstrCategory::CPU,
|
||||
MipsInstrCategory::Rsp => InstrCategory::RSP,
|
||||
MipsInstrCategory::R3000Gte => InstrCategory::R3000GTE,
|
||||
MipsInstrCategory::R4000Allegrex => InstrCategory::R4000ALLEGREX,
|
||||
MipsInstrCategory::R5900 => InstrCategory::R5900,
|
||||
};
|
||||
|
||||
let start_address = symbol.address;
|
||||
let end_address = symbol.address + symbol.size;
|
||||
let start_address = address;
|
||||
let end_address = address + code.len() as u64;
|
||||
let ins_count = code.len() / 4;
|
||||
let mut ops = Vec::<u16>::with_capacity(ins_count);
|
||||
let mut insts = Vec::<ObjIns>::with_capacity(ins_count);
|
||||
let mut cur_addr = start_address as u32;
|
||||
for chunk in code.chunks_exact(4) {
|
||||
let reloc = section.relocations.iter().find(|r| (r.address as u32 & !3) == cur_addr);
|
||||
let reloc = relocations.iter().find(|r| (r.address as u32 & !3) == cur_addr);
|
||||
let code = self.endianness.read_u32_bytes(chunk.try_into()?);
|
||||
let instruction = Instruction::new(code, cur_addr, InstrCategory::CPU);
|
||||
let instruction = Instruction::new(code, cur_addr, instr_category);
|
||||
|
||||
let formatted = instruction.disassemble(None, 0);
|
||||
let op = instruction.unique_id as u16;
|
||||
@@ -57,7 +122,7 @@ impl ObjArch for ObjArchMips {
|
||||
let mnemonic = instruction.opcode_name().to_string();
|
||||
let is_branch = instruction.is_branch();
|
||||
let branch_offset = instruction.branch_offset();
|
||||
let branch_dest = if is_branch {
|
||||
let mut branch_dest = if is_branch {
|
||||
cur_addr.checked_add_signed(branch_offset).map(|a| a as u64)
|
||||
} else {
|
||||
None
|
||||
@@ -74,9 +139,7 @@ impl ObjArch for ObjArchMips {
|
||||
OperandType::cpu_immediate
|
||||
| OperandType::cpu_label
|
||||
| OperandType::cpu_branch_target_label => {
|
||||
if let Some(branch_dest) = branch_dest {
|
||||
args.push(ObjInsArg::BranchDest(branch_dest));
|
||||
} else if let Some(reloc) = reloc {
|
||||
if let Some(reloc) = reloc {
|
||||
if matches!(&reloc.target_section, Some(s) if s == ".text")
|
||||
&& reloc.target.address > start_address
|
||||
&& reloc.target.address < end_address
|
||||
@@ -84,7 +147,10 @@ impl ObjArch for ObjArchMips {
|
||||
args.push(ObjInsArg::BranchDest(reloc.target.address));
|
||||
} else {
|
||||
push_reloc(&mut args, reloc)?;
|
||||
branch_dest = None;
|
||||
}
|
||||
} else if let Some(branch_dest) = branch_dest {
|
||||
args.push(ObjInsArg::BranchDest(branch_dest));
|
||||
} else {
|
||||
args.push(ObjInsArg::Arg(ObjInsArgValue::Opaque(
|
||||
op.disassemble(&instruction, None).into(),
|
||||
@@ -105,6 +171,18 @@ impl ObjArch for ObjArchMips {
|
||||
)));
|
||||
args.push(ObjInsArg::PlainText(")".into()));
|
||||
}
|
||||
// OperandType::r5900_immediate15 => match reloc {
|
||||
// Some(reloc)
|
||||
// if reloc.flags == RelocationFlags::Elf { r_type: R_MIPS15_S3 } =>
|
||||
// {
|
||||
// push_reloc(&mut args, reloc)?;
|
||||
// }
|
||||
// _ => {
|
||||
// args.push(ObjInsArg::Arg(ObjInsArgValue::Opaque(
|
||||
// op.disassemble(&instruction, None).into(),
|
||||
// )));
|
||||
// }
|
||||
// },
|
||||
_ => {
|
||||
args.push(ObjInsArg::Arg(ObjInsArgValue::Opaque(
|
||||
op.disassemble(&instruction, None).into(),
|
||||
@@ -112,7 +190,7 @@ impl ObjArch for ObjArchMips {
|
||||
}
|
||||
}
|
||||
}
|
||||
let line = section.line_info.range(..=cur_addr as u64).last().map(|(_, &b)| b);
|
||||
let line = line_info.range(..=cur_addr as u64).last().map(|(_, &b)| b);
|
||||
insts.push(ObjIns {
|
||||
address: cur_addr as u64,
|
||||
size: 4,
|
||||
@@ -132,6 +210,7 @@ impl ObjArch for ObjArchMips {
|
||||
|
||||
fn implcit_addend(
|
||||
&self,
|
||||
file: &File<'_>,
|
||||
section: &ObjSection,
|
||||
address: u64,
|
||||
reloc: &Relocation,
|
||||
@@ -140,14 +219,29 @@ impl ObjArch for ObjArchMips {
|
||||
let addend = self.endianness.read_u32_bytes(data);
|
||||
Ok(match reloc.flags() {
|
||||
RelocationFlags::Elf { r_type: elf::R_MIPS_32 } => addend as i64,
|
||||
RelocationFlags::Elf { r_type: elf::R_MIPS_26 } => ((addend & 0x03FFFFFF) << 2) as i64,
|
||||
RelocationFlags::Elf { r_type: elf::R_MIPS_HI16 } => {
|
||||
((addend & 0x0000FFFF) << 16) as i32 as i64
|
||||
}
|
||||
RelocationFlags::Elf {
|
||||
r_type:
|
||||
elf::R_MIPS_LO16 | elf::R_MIPS_GOT16 | elf::R_MIPS_CALL16 | elf::R_MIPS_GPREL16,
|
||||
r_type: elf::R_MIPS_LO16 | elf::R_MIPS_GOT16 | elf::R_MIPS_CALL16,
|
||||
} => (addend & 0x0000FFFF) as i16 as i64,
|
||||
RelocationFlags::Elf { r_type: elf::R_MIPS_26 } => ((addend & 0x03FFFFFF) << 2) as i64,
|
||||
RelocationFlags::Elf { r_type: elf::R_MIPS_GPREL16 | elf::R_MIPS_LITERAL } => {
|
||||
let RelocationTarget::Symbol(idx) = reloc.target() else {
|
||||
bail!("Unsupported R_MIPS_GPREL16 relocation against a non-symbol");
|
||||
};
|
||||
let sym = file.symbol_by_index(idx)?;
|
||||
|
||||
// if the symbol we are relocating against is in a local section we need to add
|
||||
// the ri_gp_value from .reginfo to the addend.
|
||||
if sym.section().index().is_some() {
|
||||
((addend & 0x0000FFFF) as i16 as i64) + self.ri_gp_value as i64
|
||||
} else {
|
||||
(addend & 0x0000FFFF) as i16 as i64
|
||||
}
|
||||
}
|
||||
RelocationFlags::Elf { r_type: elf::R_MIPS_PC16 } => 0, // PC-relative relocation
|
||||
RelocationFlags::Elf { r_type: R_MIPS15_S3 } => ((addend & 0x001FFFC0) >> 3) as i64,
|
||||
flags => bail!("Unsupported MIPS implicit relocation {flags:?}"),
|
||||
})
|
||||
}
|
||||
@@ -155,13 +249,16 @@ impl ObjArch for ObjArchMips {
|
||||
fn display_reloc(&self, flags: RelocationFlags) -> Cow<'static, str> {
|
||||
match flags {
|
||||
RelocationFlags::Elf { r_type } => match r_type {
|
||||
elf::R_MIPS_HI16 => Cow::Borrowed("R_MIPS_HI16"),
|
||||
elf::R_MIPS_LO16 => Cow::Borrowed("R_MIPS_LO16"),
|
||||
elf::R_MIPS_GOT16 => Cow::Borrowed("R_MIPS_GOT16"),
|
||||
elf::R_MIPS_CALL16 => Cow::Borrowed("R_MIPS_CALL16"),
|
||||
elf::R_MIPS_GPREL16 => Cow::Borrowed("R_MIPS_GPREL16"),
|
||||
elf::R_MIPS_32 => Cow::Borrowed("R_MIPS_32"),
|
||||
elf::R_MIPS_26 => Cow::Borrowed("R_MIPS_26"),
|
||||
elf::R_MIPS_HI16 => Cow::Borrowed("R_MIPS_HI16"),
|
||||
elf::R_MIPS_LO16 => Cow::Borrowed("R_MIPS_LO16"),
|
||||
elf::R_MIPS_GPREL16 => Cow::Borrowed("R_MIPS_GPREL16"),
|
||||
elf::R_MIPS_LITERAL => Cow::Borrowed("R_MIPS_LITERAL"),
|
||||
elf::R_MIPS_GOT16 => Cow::Borrowed("R_MIPS_GOT16"),
|
||||
elf::R_MIPS_PC16 => Cow::Borrowed("R_MIPS_PC16"),
|
||||
elf::R_MIPS_CALL16 => Cow::Borrowed("R_MIPS_CALL16"),
|
||||
R_MIPS15_S3 => Cow::Borrowed("R_MIPS15_S3"),
|
||||
_ => Cow::Owned(format!("<{flags:?}>")),
|
||||
},
|
||||
_ => Cow::Owned(format!("<{flags:?}>")),
|
||||
@@ -197,7 +294,11 @@ fn push_reloc(args: &mut Vec<ObjInsArg>, reloc: &ObjReloc) -> Result<()> {
|
||||
args.push(ObjInsArg::Reloc);
|
||||
args.push(ObjInsArg::PlainText(")".into()));
|
||||
}
|
||||
elf::R_MIPS_32 | elf::R_MIPS_26 => {
|
||||
elf::R_MIPS_32
|
||||
| elf::R_MIPS_26
|
||||
| elf::R_MIPS_LITERAL
|
||||
| elf::R_MIPS_PC16
|
||||
| R_MIPS15_S3 => {
|
||||
args.push(ObjInsArg::Reloc);
|
||||
}
|
||||
_ => bail!("Unsupported ELF MIPS relocation type {r_type}"),
|
||||
|
||||
@@ -1,34 +1,46 @@
|
||||
use std::borrow::Cow;
|
||||
use std::{borrow::Cow, collections::BTreeMap};
|
||||
|
||||
use anyhow::{bail, Result};
|
||||
use object::{Architecture, Object, Relocation, RelocationFlags};
|
||||
use object::{Architecture, File, Object, ObjectSymbol, Relocation, RelocationFlags, Symbol};
|
||||
|
||||
use crate::{
|
||||
diff::DiffObjConfig,
|
||||
obj::{ObjInfo, ObjIns, ObjSection, SymbolRef},
|
||||
obj::{ObjIns, ObjReloc, ObjSection},
|
||||
};
|
||||
|
||||
#[cfg(feature = "arm")]
|
||||
mod arm;
|
||||
#[cfg(feature = "mips")]
|
||||
mod mips;
|
||||
pub mod mips;
|
||||
#[cfg(feature = "ppc")]
|
||||
mod ppc;
|
||||
pub mod ppc;
|
||||
#[cfg(feature = "x86")]
|
||||
mod x86;
|
||||
pub mod x86;
|
||||
|
||||
pub trait ObjArch: Send + Sync {
|
||||
fn process_code(
|
||||
&self,
|
||||
obj: &ObjInfo,
|
||||
symbol_ref: SymbolRef,
|
||||
address: u64,
|
||||
code: &[u8],
|
||||
section_index: usize,
|
||||
relocations: &[ObjReloc],
|
||||
line_info: &BTreeMap<u64, u32>,
|
||||
config: &DiffObjConfig,
|
||||
) -> Result<ProcessCodeResult>;
|
||||
|
||||
fn implcit_addend(&self, section: &ObjSection, address: u64, reloc: &Relocation)
|
||||
-> Result<i64>;
|
||||
fn implcit_addend(
|
||||
&self,
|
||||
file: &File<'_>,
|
||||
section: &ObjSection,
|
||||
address: u64,
|
||||
reloc: &Relocation,
|
||||
) -> Result<i64>;
|
||||
|
||||
fn demangle(&self, _name: &str) -> Option<String> { None }
|
||||
|
||||
fn display_reloc(&self, flags: RelocationFlags) -> Cow<'static, str>;
|
||||
|
||||
fn symbol_address(&self, symbol: &Symbol) -> u64 { symbol.address() }
|
||||
}
|
||||
|
||||
pub struct ProcessCodeResult {
|
||||
@@ -44,6 +56,8 @@ pub fn new_arch(object: &object::File) -> Result<Box<dyn ObjArch>> {
|
||||
Architecture::Mips => Box::new(mips::ObjArchMips::new(object)?),
|
||||
#[cfg(feature = "x86")]
|
||||
Architecture::I386 | Architecture::X86_64 => Box::new(x86::ObjArchX86::new(object)?),
|
||||
#[cfg(feature = "arm")]
|
||||
Architecture::Arm => Box::new(arm::ObjArchArm::new(object)?),
|
||||
arch => bail!("Unsupported architecture: {arch:?}"),
|
||||
})
|
||||
}
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
use std::borrow::Cow;
|
||||
use std::{borrow::Cow, collections::BTreeMap};
|
||||
|
||||
use anyhow::{anyhow, bail, Result};
|
||||
use anyhow::{bail, Result};
|
||||
use object::{elf, File, Relocation, RelocationFlags};
|
||||
use ppc750cl::{Argument, InsIter, GPR};
|
||||
|
||||
use crate::{
|
||||
arch::{ObjArch, ProcessCodeResult},
|
||||
diff::DiffObjConfig,
|
||||
obj::{ObjInfo, ObjIns, ObjInsArg, ObjInsArgValue, ObjReloc, ObjSection, SymbolRef},
|
||||
obj::{ObjIns, ObjInsArg, ObjInsArgValue, ObjReloc, ObjSection},
|
||||
};
|
||||
|
||||
// Relative relocation, can be Simm, Offset or BranchDest
|
||||
@@ -31,20 +31,18 @@ impl ObjArchPpc {
|
||||
impl ObjArch for ObjArchPpc {
|
||||
fn process_code(
|
||||
&self,
|
||||
obj: &ObjInfo,
|
||||
symbol_ref: SymbolRef,
|
||||
address: u64,
|
||||
code: &[u8],
|
||||
_section_index: usize,
|
||||
relocations: &[ObjReloc],
|
||||
line_info: &BTreeMap<u64, u32>,
|
||||
config: &DiffObjConfig,
|
||||
) -> Result<ProcessCodeResult> {
|
||||
let (section, symbol) = obj.section_symbol(symbol_ref);
|
||||
let section = section.ok_or_else(|| anyhow!("Code symbol section not found"))?;
|
||||
let code = §ion.data
|
||||
[symbol.section_address as usize..(symbol.section_address + symbol.size) as usize];
|
||||
|
||||
let ins_count = code.len() / 4;
|
||||
let mut ops = Vec::<u16>::with_capacity(ins_count);
|
||||
let mut insts = Vec::<ObjIns>::with_capacity(ins_count);
|
||||
for (cur_addr, mut ins) in InsIter::new(code, symbol.address as u32) {
|
||||
let reloc = section.relocations.iter().find(|r| (r.address as u32 & !3) == cur_addr);
|
||||
for (cur_addr, mut ins) in InsIter::new(code, address as u32) {
|
||||
let reloc = relocations.iter().find(|r| (r.address as u32 & !3) == cur_addr);
|
||||
if let Some(reloc) = reloc {
|
||||
// Zero out relocations
|
||||
ins.code = match reloc.flags {
|
||||
@@ -133,7 +131,7 @@ impl ObjArch for ObjArchPpc {
|
||||
}
|
||||
|
||||
ops.push(ins.op as u16);
|
||||
let line = section.line_info.range(..=cur_addr as u64).last().map(|(_, &b)| b);
|
||||
let line = line_info.range(..=cur_addr as u64).last().map(|(_, &b)| b);
|
||||
insts.push(ObjIns {
|
||||
address: cur_addr as u64,
|
||||
size: 4,
|
||||
@@ -152,6 +150,7 @@ impl ObjArch for ObjArchPpc {
|
||||
|
||||
fn implcit_addend(
|
||||
&self,
|
||||
_file: &File<'_>,
|
||||
_section: &ObjSection,
|
||||
address: u64,
|
||||
reloc: &Relocation,
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
use std::borrow::Cow;
|
||||
use std::{borrow::Cow, collections::BTreeMap};
|
||||
|
||||
use anyhow::{anyhow, bail, ensure, Result};
|
||||
use iced_x86::{
|
||||
@@ -11,7 +11,7 @@ use object::{pe, Endian, Endianness, File, Object, Relocation, RelocationFlags};
|
||||
use crate::{
|
||||
arch::{ObjArch, ProcessCodeResult},
|
||||
diff::{DiffObjConfig, X86Formatter},
|
||||
obj::{ObjInfo, ObjIns, ObjInsArg, ObjInsArgValue, ObjSection, SymbolRef},
|
||||
obj::{ObjIns, ObjInsArg, ObjInsArgValue, ObjReloc, ObjSection},
|
||||
};
|
||||
|
||||
pub struct ObjArchX86 {
|
||||
@@ -28,17 +28,15 @@ impl ObjArchX86 {
|
||||
impl ObjArch for ObjArchX86 {
|
||||
fn process_code(
|
||||
&self,
|
||||
obj: &ObjInfo,
|
||||
symbol_ref: SymbolRef,
|
||||
address: u64,
|
||||
code: &[u8],
|
||||
_section_index: usize,
|
||||
relocations: &[ObjReloc],
|
||||
line_info: &BTreeMap<u64, u32>,
|
||||
config: &DiffObjConfig,
|
||||
) -> Result<ProcessCodeResult> {
|
||||
let (section, symbol) = obj.section_symbol(symbol_ref);
|
||||
let section = section.ok_or_else(|| anyhow!("Code symbol section not found"))?;
|
||||
let code = §ion.data
|
||||
[symbol.section_address as usize..(symbol.section_address + symbol.size) as usize];
|
||||
|
||||
let mut result = ProcessCodeResult { ops: Vec::new(), insts: Vec::new() };
|
||||
let mut decoder = Decoder::with_ip(self.bits, code, symbol.address, DecoderOptions::NONE);
|
||||
let mut decoder = Decoder::with_ip(self.bits, code, address, DecoderOptions::NONE);
|
||||
let mut formatter: Box<dyn Formatter> = match config.x86_formatter {
|
||||
X86Formatter::Intel => Box::new(IntelFormatter::new()),
|
||||
X86Formatter::Gas => Box::new(GasFormatter::new()),
|
||||
@@ -70,11 +68,10 @@ impl ObjArch for ObjArchX86 {
|
||||
|
||||
let address = instruction.ip();
|
||||
let op = instruction.mnemonic() as u16;
|
||||
let reloc = section
|
||||
.relocations
|
||||
let reloc = relocations
|
||||
.iter()
|
||||
.find(|r| r.address >= address && r.address < address + instruction.len() as u64);
|
||||
let line = section.line_info.range(..=address).last().map(|(_, &b)| b);
|
||||
let line = line_info.range(..=address).last().map(|(_, &b)| b);
|
||||
output.ins = ObjIns {
|
||||
address,
|
||||
size: instruction.len() as u8,
|
||||
@@ -131,6 +128,7 @@ impl ObjArch for ObjArchX86 {
|
||||
|
||||
fn implcit_addend(
|
||||
&self,
|
||||
_file: &File<'_>,
|
||||
section: &ObjSection,
|
||||
address: u64,
|
||||
reloc: &Relocation,
|
||||
|
||||
244
objdiff-core/src/bindings/diff.rs
Normal file
244
objdiff-core/src/bindings/diff.rs
Normal file
@@ -0,0 +1,244 @@
|
||||
use crate::{
|
||||
diff::{
|
||||
ObjDataDiff, ObjDataDiffKind, ObjDiff, ObjInsArgDiff, ObjInsBranchFrom, ObjInsBranchTo,
|
||||
ObjInsDiff, ObjInsDiffKind, ObjSectionDiff, ObjSymbolDiff,
|
||||
},
|
||||
obj::{
|
||||
ObjInfo, ObjIns, ObjInsArg, ObjInsArgValue, ObjReloc, ObjSectionKind, ObjSymbol,
|
||||
ObjSymbolFlagSet, ObjSymbolFlags,
|
||||
},
|
||||
};
|
||||
|
||||
// Protobuf diff types
|
||||
include!(concat!(env!("OUT_DIR"), "/objdiff.diff.rs"));
|
||||
include!(concat!(env!("OUT_DIR"), "/objdiff.diff.serde.rs"));
|
||||
|
||||
impl DiffResult {
|
||||
pub fn new(left: Option<(&ObjInfo, &ObjDiff)>, right: Option<(&ObjInfo, &ObjDiff)>) -> Self {
|
||||
Self {
|
||||
left: left.map(|(obj, diff)| ObjectDiff::new(obj, diff)),
|
||||
right: right.map(|(obj, diff)| ObjectDiff::new(obj, diff)),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl ObjectDiff {
|
||||
pub fn new(obj: &ObjInfo, diff: &ObjDiff) -> Self {
|
||||
Self {
|
||||
sections: diff
|
||||
.sections
|
||||
.iter()
|
||||
.enumerate()
|
||||
.map(|(i, d)| SectionDiff::new(obj, i, d))
|
||||
.collect(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl SectionDiff {
|
||||
pub fn new(obj: &ObjInfo, section_index: usize, section_diff: &ObjSectionDiff) -> Self {
|
||||
let section = &obj.sections[section_index];
|
||||
let functions = section_diff.symbols.iter().map(|d| FunctionDiff::new(obj, d)).collect();
|
||||
let data = section_diff.data_diff.iter().map(|d| DataDiff::new(obj, d)).collect();
|
||||
Self {
|
||||
name: section.name.to_string(),
|
||||
kind: SectionKind::from(section.kind) as i32,
|
||||
size: section.size,
|
||||
address: section.address,
|
||||
functions,
|
||||
data,
|
||||
match_percent: section_diff.match_percent,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<ObjSectionKind> for SectionKind {
|
||||
fn from(value: ObjSectionKind) -> Self {
|
||||
match value {
|
||||
ObjSectionKind::Code => SectionKind::SectionText,
|
||||
ObjSectionKind::Data => SectionKind::SectionData,
|
||||
ObjSectionKind::Bss => SectionKind::SectionBss,
|
||||
// TODO common
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl FunctionDiff {
|
||||
pub fn new(object: &ObjInfo, symbol_diff: &ObjSymbolDiff) -> Self {
|
||||
let (_section, symbol) = object.section_symbol(symbol_diff.symbol_ref);
|
||||
// let diff_symbol = symbol_diff.diff_symbol.map(|symbol_ref| {
|
||||
// let (_section, symbol) = object.section_symbol(symbol_ref);
|
||||
// Symbol::from(symbol)
|
||||
// });
|
||||
let instructions = symbol_diff.instructions.iter().map(InstructionDiff::from).collect();
|
||||
Self {
|
||||
symbol: Some(Symbol::from(symbol)),
|
||||
// diff_symbol,
|
||||
instructions,
|
||||
match_percent: symbol_diff.match_percent,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl DataDiff {
|
||||
pub fn new(_object: &ObjInfo, data_diff: &ObjDataDiff) -> Self {
|
||||
Self {
|
||||
kind: DiffKind::from(data_diff.kind) as i32,
|
||||
data: data_diff.data.clone(),
|
||||
size: data_diff.len as u64,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> From<&'a ObjSymbol> for Symbol {
|
||||
fn from(value: &'a ObjSymbol) -> Self {
|
||||
Self {
|
||||
name: value.name.to_string(),
|
||||
demangled_name: value.demangled_name.clone(),
|
||||
address: value.address,
|
||||
size: value.size,
|
||||
flags: symbol_flags(value.flags),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn symbol_flags(value: ObjSymbolFlagSet) -> u32 {
|
||||
let mut flags = 0u32;
|
||||
if value.0.contains(ObjSymbolFlags::Global) {
|
||||
flags |= SymbolFlag::SymbolNone as u32;
|
||||
}
|
||||
if value.0.contains(ObjSymbolFlags::Local) {
|
||||
flags |= SymbolFlag::SymbolLocal as u32;
|
||||
}
|
||||
if value.0.contains(ObjSymbolFlags::Weak) {
|
||||
flags |= SymbolFlag::SymbolWeak as u32;
|
||||
}
|
||||
if value.0.contains(ObjSymbolFlags::Common) {
|
||||
flags |= SymbolFlag::SymbolCommon as u32;
|
||||
}
|
||||
if value.0.contains(ObjSymbolFlags::Hidden) {
|
||||
flags |= SymbolFlag::SymbolHidden as u32;
|
||||
}
|
||||
flags
|
||||
}
|
||||
|
||||
impl<'a> From<&'a ObjIns> for Instruction {
|
||||
fn from(value: &'a ObjIns) -> Self {
|
||||
Self {
|
||||
address: value.address,
|
||||
size: value.size as u32,
|
||||
opcode: value.op as u32,
|
||||
mnemonic: value.mnemonic.clone(),
|
||||
formatted: value.formatted.clone(),
|
||||
arguments: value.args.iter().map(Argument::from).collect(),
|
||||
relocation: value.reloc.as_ref().map(Relocation::from),
|
||||
branch_dest: value.branch_dest,
|
||||
line_number: value.line,
|
||||
original: value.orig.clone(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> From<&'a ObjInsArg> for Argument {
|
||||
fn from(value: &'a ObjInsArg) -> Self {
|
||||
Self {
|
||||
value: Some(match value {
|
||||
ObjInsArg::PlainText(s) => argument::Value::PlainText(s.to_string()),
|
||||
ObjInsArg::Arg(v) => argument::Value::Argument(ArgumentValue::from(v)),
|
||||
ObjInsArg::Reloc => argument::Value::Relocation(ArgumentRelocation {}),
|
||||
ObjInsArg::BranchDest(dest) => argument::Value::BranchDest(*dest),
|
||||
}),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<&ObjInsArgValue> for ArgumentValue {
|
||||
fn from(value: &ObjInsArgValue) -> Self {
|
||||
Self {
|
||||
value: Some(match value {
|
||||
ObjInsArgValue::Signed(v) => argument_value::Value::Signed(*v),
|
||||
ObjInsArgValue::Unsigned(v) => argument_value::Value::Unsigned(*v),
|
||||
ObjInsArgValue::Opaque(v) => argument_value::Value::Opaque(v.to_string()),
|
||||
}),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> From<&'a ObjReloc> for Relocation {
|
||||
fn from(value: &ObjReloc) -> Self {
|
||||
Self {
|
||||
r#type: match value.flags {
|
||||
object::RelocationFlags::Elf { r_type } => r_type,
|
||||
object::RelocationFlags::MachO { r_type, .. } => r_type as u32,
|
||||
object::RelocationFlags::Coff { typ } => typ as u32,
|
||||
object::RelocationFlags::Xcoff { r_rtype, .. } => r_rtype as u32,
|
||||
_ => unreachable!(),
|
||||
},
|
||||
type_name: String::new(), // TODO
|
||||
target: Some(RelocationTarget::from(&value.target)),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> From<&'a ObjSymbol> for RelocationTarget {
|
||||
fn from(value: &'a ObjSymbol) -> Self {
|
||||
Self { symbol: Some(Symbol::from(value)), addend: value.addend }
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> From<&'a ObjInsDiff> for InstructionDiff {
|
||||
fn from(value: &'a ObjInsDiff) -> Self {
|
||||
Self {
|
||||
instruction: value.ins.as_ref().map(Instruction::from),
|
||||
diff_kind: DiffKind::from(value.kind) as i32,
|
||||
branch_from: value.branch_from.as_ref().map(InstructionBranchFrom::from),
|
||||
branch_to: value.branch_to.as_ref().map(InstructionBranchTo::from),
|
||||
arg_diff: value.arg_diff.iter().map(ArgumentDiff::from).collect(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<&Option<ObjInsArgDiff>> for ArgumentDiff {
|
||||
fn from(value: &Option<ObjInsArgDiff>) -> Self {
|
||||
Self { diff_index: value.as_ref().map(|v| v.idx as u32) }
|
||||
}
|
||||
}
|
||||
|
||||
impl From<ObjInsDiffKind> for DiffKind {
|
||||
fn from(value: ObjInsDiffKind) -> Self {
|
||||
match value {
|
||||
ObjInsDiffKind::None => DiffKind::DiffNone,
|
||||
ObjInsDiffKind::OpMismatch => DiffKind::DiffOpMismatch,
|
||||
ObjInsDiffKind::ArgMismatch => DiffKind::DiffArgMismatch,
|
||||
ObjInsDiffKind::Replace => DiffKind::DiffReplace,
|
||||
ObjInsDiffKind::Delete => DiffKind::DiffDelete,
|
||||
ObjInsDiffKind::Insert => DiffKind::DiffInsert,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<ObjDataDiffKind> for DiffKind {
|
||||
fn from(value: ObjDataDiffKind) -> Self {
|
||||
match value {
|
||||
ObjDataDiffKind::None => DiffKind::DiffNone,
|
||||
ObjDataDiffKind::Replace => DiffKind::DiffReplace,
|
||||
ObjDataDiffKind::Delete => DiffKind::DiffDelete,
|
||||
ObjDataDiffKind::Insert => DiffKind::DiffInsert,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> From<&'a ObjInsBranchFrom> for InstructionBranchFrom {
|
||||
fn from(value: &'a ObjInsBranchFrom) -> Self {
|
||||
Self {
|
||||
instruction_index: value.ins_idx.iter().map(|&x| x as u32).collect(),
|
||||
branch_index: value.branch_idx as u32,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> From<&'a ObjInsBranchTo> for InstructionBranchTo {
|
||||
fn from(value: &'a ObjInsBranchTo) -> Self {
|
||||
Self { instruction_index: value.ins_idx as u32, branch_index: value.branch_idx as u32 }
|
||||
}
|
||||
}
|
||||
5
objdiff-core/src/bindings/mod.rs
Normal file
5
objdiff-core/src/bindings/mod.rs
Normal file
@@ -0,0 +1,5 @@
|
||||
#[cfg(feature = "any-arch")]
|
||||
pub mod diff;
|
||||
pub mod report;
|
||||
#[cfg(feature = "wasm")]
|
||||
pub mod wasm;
|
||||
332
objdiff-core/src/bindings/report.rs
Normal file
332
objdiff-core/src/bindings/report.rs
Normal file
@@ -0,0 +1,332 @@
|
||||
use std::ops::AddAssign;
|
||||
|
||||
use anyhow::{bail, Result};
|
||||
use prost::Message;
|
||||
use serde_json::error::Category;
|
||||
|
||||
// Protobuf report types
|
||||
include!(concat!(env!("OUT_DIR"), "/objdiff.report.rs"));
|
||||
include!(concat!(env!("OUT_DIR"), "/objdiff.report.serde.rs"));
|
||||
|
||||
pub const REPORT_VERSION: u32 = 1;
|
||||
|
||||
impl Report {
|
||||
pub fn parse(data: &[u8]) -> Result<Self> {
|
||||
if data.is_empty() {
|
||||
bail!(std::io::Error::from(std::io::ErrorKind::UnexpectedEof));
|
||||
}
|
||||
let report = if data[0] == b'{' {
|
||||
// Load as JSON
|
||||
Self::from_json(data)?
|
||||
} else {
|
||||
// Load as binary protobuf
|
||||
Self::decode(data)?
|
||||
};
|
||||
Ok(report)
|
||||
}
|
||||
|
||||
fn from_json(bytes: &[u8]) -> Result<Self, serde_json::Error> {
|
||||
match serde_json::from_slice::<Self>(bytes) {
|
||||
Ok(report) => Ok(report),
|
||||
Err(e) => {
|
||||
match e.classify() {
|
||||
Category::Io | Category::Eof | Category::Syntax => Err(e),
|
||||
Category::Data => {
|
||||
// Try to load as legacy report
|
||||
match serde_json::from_slice::<LegacyReport>(bytes) {
|
||||
Ok(legacy_report) => Ok(Report::from(legacy_report)),
|
||||
Err(_) => Err(e),
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn migrate(&mut self) -> Result<()> {
|
||||
if self.version == 0 {
|
||||
self.migrate_v0()?;
|
||||
}
|
||||
if self.version != REPORT_VERSION {
|
||||
bail!("Unsupported report version: {}", self.version);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn migrate_v0(&mut self) -> Result<()> {
|
||||
let Some(measures) = &mut self.measures else {
|
||||
bail!("Missing measures in report");
|
||||
};
|
||||
for unit in &mut self.units {
|
||||
let Some(unit_measures) = &mut unit.measures else {
|
||||
bail!("Missing measures in report unit");
|
||||
};
|
||||
let Some(metadata) = &mut unit.metadata else {
|
||||
bail!("Missing metadata in report unit");
|
||||
};
|
||||
if metadata.module_name.is_some() || metadata.module_id.is_some() {
|
||||
metadata.progress_categories = vec!["modules".to_string()];
|
||||
} else {
|
||||
metadata.progress_categories = vec!["dol".to_string()];
|
||||
}
|
||||
if metadata.complete.unwrap_or(false) {
|
||||
unit_measures.complete_code = unit_measures.total_code;
|
||||
unit_measures.complete_data = unit_measures.total_data;
|
||||
unit_measures.complete_code_percent = 100.0;
|
||||
unit_measures.complete_data_percent = 100.0;
|
||||
} else {
|
||||
unit_measures.complete_code = 0;
|
||||
unit_measures.complete_data = 0;
|
||||
unit_measures.complete_code_percent = 0.0;
|
||||
unit_measures.complete_data_percent = 0.0;
|
||||
}
|
||||
measures.complete_code += unit_measures.complete_code;
|
||||
measures.complete_data += unit_measures.complete_data;
|
||||
}
|
||||
measures.calc_matched_percent();
|
||||
self.version = 1;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn calculate_progress_categories(&mut self) {
|
||||
for unit in &self.units {
|
||||
let Some(metadata) = unit.metadata.as_ref() else {
|
||||
continue;
|
||||
};
|
||||
let Some(measures) = unit.measures.as_ref() else {
|
||||
continue;
|
||||
};
|
||||
for category_id in &metadata.progress_categories {
|
||||
let category = match self.categories.iter_mut().find(|c| &c.id == category_id) {
|
||||
Some(category) => category,
|
||||
None => {
|
||||
self.categories.push(ReportCategory {
|
||||
id: category_id.clone(),
|
||||
name: String::new(),
|
||||
measures: Some(Default::default()),
|
||||
});
|
||||
self.categories.last_mut().unwrap()
|
||||
}
|
||||
};
|
||||
*category.measures.get_or_insert_with(Default::default) += *measures;
|
||||
}
|
||||
}
|
||||
for category in &mut self.categories {
|
||||
let measures = category.measures.get_or_insert_with(Default::default);
|
||||
measures.calc_fuzzy_match_percent();
|
||||
measures.calc_matched_percent();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Measures {
|
||||
/// Average the fuzzy match percentage over total code bytes.
|
||||
pub fn calc_fuzzy_match_percent(&mut self) {
|
||||
if self.total_code == 0 {
|
||||
self.fuzzy_match_percent = 100.0;
|
||||
} else {
|
||||
self.fuzzy_match_percent /= self.total_code as f32;
|
||||
}
|
||||
}
|
||||
|
||||
/// Calculate the percentage of matched code, data, and functions.
|
||||
pub fn calc_matched_percent(&mut self) {
|
||||
self.matched_code_percent = if self.total_code == 0 {
|
||||
100.0
|
||||
} else {
|
||||
self.matched_code as f32 / self.total_code as f32 * 100.0
|
||||
};
|
||||
self.matched_data_percent = if self.total_data == 0 {
|
||||
100.0
|
||||
} else {
|
||||
self.matched_data as f32 / self.total_data as f32 * 100.0
|
||||
};
|
||||
self.matched_functions_percent = if self.total_functions == 0 {
|
||||
100.0
|
||||
} else {
|
||||
self.matched_functions as f32 / self.total_functions as f32 * 100.0
|
||||
};
|
||||
self.complete_code_percent = if self.total_code == 0 {
|
||||
100.0
|
||||
} else {
|
||||
self.complete_code as f32 / self.total_code as f32 * 100.0
|
||||
};
|
||||
self.complete_data_percent = if self.total_data == 0 {
|
||||
100.0
|
||||
} else {
|
||||
self.complete_data as f32 / self.total_data as f32 * 100.0
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
impl From<&ReportItem> for ChangeItemInfo {
|
||||
fn from(value: &ReportItem) -> Self {
|
||||
Self { fuzzy_match_percent: value.fuzzy_match_percent, size: value.size }
|
||||
}
|
||||
}
|
||||
|
||||
impl AddAssign for Measures {
|
||||
fn add_assign(&mut self, other: Self) {
|
||||
self.fuzzy_match_percent += other.fuzzy_match_percent * other.total_code as f32;
|
||||
self.total_code += other.total_code;
|
||||
self.matched_code += other.matched_code;
|
||||
self.total_data += other.total_data;
|
||||
self.matched_data += other.matched_data;
|
||||
self.total_functions += other.total_functions;
|
||||
self.matched_functions += other.matched_functions;
|
||||
self.complete_code += other.complete_code;
|
||||
self.complete_data += other.complete_data;
|
||||
}
|
||||
}
|
||||
|
||||
/// Allows [collect](Iterator::collect) to be used on an iterator of [Measures].
|
||||
impl FromIterator<Measures> for Measures {
|
||||
fn from_iter<T>(iter: T) -> Self
|
||||
where T: IntoIterator<Item = Measures> {
|
||||
let mut measures = Measures::default();
|
||||
for other in iter {
|
||||
measures += other;
|
||||
}
|
||||
measures.calc_fuzzy_match_percent();
|
||||
measures.calc_matched_percent();
|
||||
measures
|
||||
}
|
||||
}
|
||||
|
||||
// Older JSON report types
|
||||
#[derive(Debug, Clone, Default, serde::Serialize, serde::Deserialize)]
|
||||
struct LegacyReport {
|
||||
fuzzy_match_percent: f32,
|
||||
total_code: u64,
|
||||
matched_code: u64,
|
||||
matched_code_percent: f32,
|
||||
total_data: u64,
|
||||
matched_data: u64,
|
||||
matched_data_percent: f32,
|
||||
total_functions: u32,
|
||||
matched_functions: u32,
|
||||
matched_functions_percent: f32,
|
||||
units: Vec<LegacyReportUnit>,
|
||||
}
|
||||
|
||||
impl From<LegacyReport> for Report {
|
||||
fn from(value: LegacyReport) -> Self {
|
||||
Self {
|
||||
measures: Some(Measures {
|
||||
fuzzy_match_percent: value.fuzzy_match_percent,
|
||||
total_code: value.total_code,
|
||||
matched_code: value.matched_code,
|
||||
matched_code_percent: value.matched_code_percent,
|
||||
total_data: value.total_data,
|
||||
matched_data: value.matched_data,
|
||||
matched_data_percent: value.matched_data_percent,
|
||||
total_functions: value.total_functions,
|
||||
matched_functions: value.matched_functions,
|
||||
matched_functions_percent: value.matched_functions_percent,
|
||||
..Default::default()
|
||||
}),
|
||||
units: value.units.into_iter().map(ReportUnit::from).collect::<Vec<_>>(),
|
||||
..Default::default()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Default, serde::Serialize, serde::Deserialize)]
|
||||
struct LegacyReportUnit {
|
||||
name: String,
|
||||
fuzzy_match_percent: f32,
|
||||
total_code: u64,
|
||||
matched_code: u64,
|
||||
total_data: u64,
|
||||
matched_data: u64,
|
||||
total_functions: u32,
|
||||
matched_functions: u32,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
complete: Option<bool>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
module_name: Option<String>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
module_id: Option<u32>,
|
||||
sections: Vec<LegacyReportItem>,
|
||||
functions: Vec<LegacyReportItem>,
|
||||
}
|
||||
|
||||
impl From<LegacyReportUnit> for ReportUnit {
|
||||
fn from(value: LegacyReportUnit) -> Self {
|
||||
let mut measures = Measures {
|
||||
fuzzy_match_percent: value.fuzzy_match_percent,
|
||||
total_code: value.total_code,
|
||||
matched_code: value.matched_code,
|
||||
total_data: value.total_data,
|
||||
matched_data: value.matched_data,
|
||||
total_functions: value.total_functions,
|
||||
matched_functions: value.matched_functions,
|
||||
..Default::default()
|
||||
};
|
||||
measures.calc_matched_percent();
|
||||
Self {
|
||||
name: value.name.clone(),
|
||||
measures: Some(measures),
|
||||
sections: value.sections.into_iter().map(ReportItem::from).collect(),
|
||||
functions: value.functions.into_iter().map(ReportItem::from).collect(),
|
||||
metadata: Some(ReportUnitMetadata {
|
||||
complete: value.complete,
|
||||
module_name: value.module_name.clone(),
|
||||
module_id: value.module_id,
|
||||
..Default::default()
|
||||
}),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Default, serde::Serialize, serde::Deserialize)]
|
||||
struct LegacyReportItem {
|
||||
name: String,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
demangled_name: Option<String>,
|
||||
#[serde(
|
||||
default,
|
||||
skip_serializing_if = "Option::is_none",
|
||||
serialize_with = "serialize_hex",
|
||||
deserialize_with = "deserialize_hex"
|
||||
)]
|
||||
address: Option<u64>,
|
||||
size: u64,
|
||||
fuzzy_match_percent: f32,
|
||||
}
|
||||
|
||||
impl From<LegacyReportItem> for ReportItem {
|
||||
fn from(value: LegacyReportItem) -> Self {
|
||||
Self {
|
||||
name: value.name,
|
||||
size: value.size,
|
||||
fuzzy_match_percent: value.fuzzy_match_percent,
|
||||
metadata: Some(ReportItemMetadata {
|
||||
demangled_name: value.demangled_name,
|
||||
virtual_address: value.address,
|
||||
}),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn serialize_hex<S>(x: &Option<u64>, s: S) -> Result<S::Ok, S::Error>
|
||||
where S: serde::Serializer {
|
||||
if let Some(x) = x {
|
||||
s.serialize_str(&format!("{:#x}", x))
|
||||
} else {
|
||||
s.serialize_none()
|
||||
}
|
||||
}
|
||||
|
||||
fn deserialize_hex<'de, D>(d: D) -> Result<Option<u64>, D::Error>
|
||||
where D: serde::Deserializer<'de> {
|
||||
use serde::Deserialize;
|
||||
let s = String::deserialize(d)?;
|
||||
if s.is_empty() {
|
||||
Ok(None)
|
||||
} else if !s.starts_with("0x") {
|
||||
Err(serde::de::Error::custom("expected hex string"))
|
||||
} else {
|
||||
u64::from_str_radix(&s[2..], 16).map(Some).map_err(serde::de::Error::custom)
|
||||
}
|
||||
}
|
||||
78
objdiff-core/src/bindings/wasm.rs
Normal file
78
objdiff-core/src/bindings/wasm.rs
Normal file
@@ -0,0 +1,78 @@
|
||||
use prost::Message;
|
||||
use wasm_bindgen::prelude::*;
|
||||
|
||||
use crate::{bindings::diff::DiffResult, diff, obj};
|
||||
|
||||
fn parse_object(
|
||||
data: Option<Box<[u8]>>,
|
||||
config: &diff::DiffObjConfig,
|
||||
) -> Result<Option<obj::ObjInfo>, JsError> {
|
||||
data.as_ref().map(|data| obj::read::parse(data, config)).transpose().to_js()
|
||||
}
|
||||
|
||||
fn parse_and_run_diff(
|
||||
left: Option<Box<[u8]>>,
|
||||
right: Option<Box<[u8]>>,
|
||||
config: diff::DiffObjConfig,
|
||||
) -> Result<DiffResult, JsError> {
|
||||
let target = parse_object(left, &config)?;
|
||||
let base = parse_object(right, &config)?;
|
||||
run_diff(target.as_ref(), base.as_ref(), config)
|
||||
}
|
||||
|
||||
fn run_diff(
|
||||
left: Option<&obj::ObjInfo>,
|
||||
right: Option<&obj::ObjInfo>,
|
||||
config: diff::DiffObjConfig,
|
||||
) -> Result<DiffResult, JsError> {
|
||||
log::debug!("Running diff with config: {:?}", config);
|
||||
let result = diff::diff_objs(&config, left, right, None).to_js()?;
|
||||
let left = left.and_then(|o| result.left.as_ref().map(|d| (o, d)));
|
||||
let right = right.and_then(|o| result.right.as_ref().map(|d| (o, d)));
|
||||
Ok(DiffResult::new(left, right))
|
||||
}
|
||||
|
||||
// #[wasm_bindgen]
|
||||
// pub fn run_diff_json(
|
||||
// left: Option<Box<[u8]>>,
|
||||
// right: Option<Box<[u8]>>,
|
||||
// config: diff::DiffObjConfig,
|
||||
// ) -> Result<String, JsError> {
|
||||
// let out = run_diff_opt_box(left, right, config)?;
|
||||
// serde_json::to_string(&out).map_err(|e| JsError::new(&e.to_string()))
|
||||
// }
|
||||
|
||||
#[wasm_bindgen]
|
||||
pub fn run_diff_proto(
|
||||
left: Option<Box<[u8]>>,
|
||||
right: Option<Box<[u8]>>,
|
||||
config: diff::DiffObjConfig,
|
||||
) -> Result<Box<[u8]>, JsError> {
|
||||
let out = parse_and_run_diff(left, right, config)?;
|
||||
Ok(out.encode_to_vec().into_boxed_slice())
|
||||
}
|
||||
|
||||
#[wasm_bindgen(start)]
|
||||
fn start() -> Result<(), JsError> {
|
||||
console_error_panic_hook::set_once();
|
||||
#[cfg(debug_assertions)]
|
||||
console_log::init_with_level(log::Level::Debug).to_js()?;
|
||||
#[cfg(not(debug_assertions))]
|
||||
console_log::init_with_level(log::Level::Info).to_js()?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn to_js_error(e: impl std::fmt::Display) -> JsError { JsError::new(&e.to_string()) }
|
||||
|
||||
trait ToJsResult {
|
||||
type Output;
|
||||
|
||||
fn to_js(self) -> Result<Self::Output, JsError>;
|
||||
}
|
||||
|
||||
impl<T, E: std::fmt::Display> ToJsResult for Result<T, E> {
|
||||
type Output = T;
|
||||
|
||||
fn to_js(self) -> Result<T, JsError> { self.map_err(to_js_error) }
|
||||
}
|
||||
@@ -31,6 +31,8 @@ pub struct ProjectConfig {
|
||||
pub watch_patterns: Option<Vec<Glob>>,
|
||||
#[serde(default, alias = "units")]
|
||||
pub objects: Vec<ProjectObject>,
|
||||
#[serde(default)]
|
||||
pub progress_categories: Vec<ProjectProgressCategory>,
|
||||
}
|
||||
|
||||
#[derive(Default, Clone, serde::Deserialize)]
|
||||
@@ -44,11 +46,37 @@ pub struct ProjectObject {
|
||||
#[serde(default)]
|
||||
pub base_path: Option<PathBuf>,
|
||||
#[serde(default)]
|
||||
#[deprecated(note = "Use metadata.reverse_fn_order")]
|
||||
pub reverse_fn_order: Option<bool>,
|
||||
#[serde(default)]
|
||||
#[deprecated(note = "Use metadata.complete")]
|
||||
pub complete: Option<bool>,
|
||||
#[serde(default)]
|
||||
pub scratch: Option<ScratchConfig>,
|
||||
#[serde(default)]
|
||||
pub metadata: Option<ProjectObjectMetadata>,
|
||||
}
|
||||
|
||||
#[derive(Default, Clone, serde::Deserialize)]
|
||||
pub struct ProjectObjectMetadata {
|
||||
#[serde(default)]
|
||||
pub complete: Option<bool>,
|
||||
#[serde(default)]
|
||||
pub reverse_fn_order: Option<bool>,
|
||||
#[serde(default)]
|
||||
pub source_path: Option<String>,
|
||||
#[serde(default)]
|
||||
pub progress_categories: Option<Vec<String>>,
|
||||
#[serde(default)]
|
||||
pub auto_generated: Option<bool>,
|
||||
}
|
||||
|
||||
#[derive(Default, Clone, serde::Deserialize)]
|
||||
pub struct ProjectProgressCategory {
|
||||
#[serde(default)]
|
||||
pub id: String,
|
||||
#[serde(default)]
|
||||
pub name: String,
|
||||
}
|
||||
|
||||
impl ProjectObject {
|
||||
@@ -82,6 +110,20 @@ impl ProjectObject {
|
||||
self.base_path = Some(project_dir.join(path));
|
||||
}
|
||||
}
|
||||
|
||||
pub fn complete(&self) -> Option<bool> {
|
||||
#[allow(deprecated)]
|
||||
self.metadata.as_ref().and_then(|m| m.complete).or(self.complete)
|
||||
}
|
||||
|
||||
pub fn reverse_fn_order(&self) -> Option<bool> {
|
||||
#[allow(deprecated)]
|
||||
self.metadata.as_ref().and_then(|m| m.reverse_fn_order).or(self.reverse_fn_order)
|
||||
}
|
||||
|
||||
pub fn hidden(&self) -> bool {
|
||||
self.metadata.as_ref().and_then(|m| m.auto_generated).unwrap_or(false)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Default, Clone, Eq, PartialEq, serde::Deserialize, serde::Serialize)]
|
||||
|
||||
@@ -1,10 +1,6 @@
|
||||
use std::{
|
||||
cmp::max,
|
||||
collections::BTreeMap,
|
||||
time::{Duration, Instant},
|
||||
};
|
||||
use std::{cmp::max, collections::BTreeMap};
|
||||
|
||||
use anyhow::Result;
|
||||
use anyhow::{anyhow, Result};
|
||||
use similar::{capture_diff_slices_deadline, Algorithm};
|
||||
|
||||
use crate::{
|
||||
@@ -16,34 +12,48 @@ use crate::{
|
||||
obj::{ObjInfo, ObjInsArg, ObjReloc, ObjSymbol, ObjSymbolFlags, SymbolRef},
|
||||
};
|
||||
|
||||
pub fn no_diff_code(
|
||||
pub fn process_code_symbol(
|
||||
obj: &ObjInfo,
|
||||
symbol_ref: SymbolRef,
|
||||
config: &DiffObjConfig,
|
||||
) -> Result<ObjSymbolDiff> {
|
||||
let out = obj.arch.process_code(obj, symbol_ref, config)?;
|
||||
) -> Result<ProcessCodeResult> {
|
||||
let (section, symbol) = obj.section_symbol(symbol_ref);
|
||||
let section = section.ok_or_else(|| anyhow!("Code symbol section not found"))?;
|
||||
let code = §ion.data
|
||||
[symbol.section_address as usize..(symbol.section_address + symbol.size) as usize];
|
||||
obj.arch.process_code(
|
||||
symbol.address,
|
||||
code,
|
||||
section.orig_index,
|
||||
§ion.relocations,
|
||||
§ion.line_info,
|
||||
config,
|
||||
)
|
||||
}
|
||||
|
||||
pub fn no_diff_code(out: &ProcessCodeResult, symbol_ref: SymbolRef) -> Result<ObjSymbolDiff> {
|
||||
let mut diff = Vec::<ObjInsDiff>::new();
|
||||
for i in out.insts {
|
||||
diff.push(ObjInsDiff { ins: Some(i), kind: ObjInsDiffKind::None, ..Default::default() });
|
||||
for i in &out.insts {
|
||||
diff.push(ObjInsDiff {
|
||||
ins: Some(i.clone()),
|
||||
kind: ObjInsDiffKind::None,
|
||||
..Default::default()
|
||||
});
|
||||
}
|
||||
resolve_branches(&mut diff);
|
||||
Ok(ObjSymbolDiff { symbol_ref, diff_symbol: None, instructions: diff, match_percent: None })
|
||||
}
|
||||
|
||||
pub fn diff_code(
|
||||
left_obj: &ObjInfo,
|
||||
right_obj: &ObjInfo,
|
||||
left_out: &ProcessCodeResult,
|
||||
right_out: &ProcessCodeResult,
|
||||
left_symbol_ref: SymbolRef,
|
||||
right_symbol_ref: SymbolRef,
|
||||
config: &DiffObjConfig,
|
||||
) -> Result<(ObjSymbolDiff, ObjSymbolDiff)> {
|
||||
let left_out = left_obj.arch.process_code(left_obj, left_symbol_ref, config)?;
|
||||
let right_out = right_obj.arch.process_code(right_obj, right_symbol_ref, config)?;
|
||||
|
||||
let mut left_diff = Vec::<ObjInsDiff>::new();
|
||||
let mut right_diff = Vec::<ObjInsDiff>::new();
|
||||
diff_instructions(&mut left_diff, &mut right_diff, &left_out, &right_out)?;
|
||||
diff_instructions(&mut left_diff, &mut right_diff, left_out, right_out)?;
|
||||
|
||||
resolve_branches(&mut left_diff);
|
||||
resolve_branches(&mut right_diff);
|
||||
@@ -86,13 +96,8 @@ fn diff_instructions(
|
||||
left_code: &ProcessCodeResult,
|
||||
right_code: &ProcessCodeResult,
|
||||
) -> Result<()> {
|
||||
let deadline = Instant::now() + Duration::from_secs(5);
|
||||
let ops = capture_diff_slices_deadline(
|
||||
Algorithm::Patience,
|
||||
&left_code.ops,
|
||||
&right_code.ops,
|
||||
Some(deadline),
|
||||
);
|
||||
let ops =
|
||||
capture_diff_slices_deadline(Algorithm::Patience, &left_code.ops, &right_code.ops, None);
|
||||
if ops.is_empty() {
|
||||
left_diff.extend(
|
||||
left_code
|
||||
|
||||
@@ -1,7 +1,4 @@
|
||||
use std::{
|
||||
cmp::{max, min, Ordering},
|
||||
time::{Duration, Instant},
|
||||
};
|
||||
use std::cmp::{max, min, Ordering};
|
||||
|
||||
use anyhow::{anyhow, Result};
|
||||
use similar::{capture_diff_slices_deadline, get_diff_ratio, Algorithm};
|
||||
@@ -11,27 +8,6 @@ use crate::{
|
||||
obj::{ObjInfo, ObjSection, SymbolRef},
|
||||
};
|
||||
|
||||
/// Compare the addresses and sizes of each symbol in the BSS sections.
|
||||
pub fn diff_bss_section(
|
||||
left: &ObjSection,
|
||||
right: &ObjSection,
|
||||
) -> Result<(ObjSectionDiff, ObjSectionDiff)> {
|
||||
let deadline = Instant::now() + Duration::from_secs(5);
|
||||
let left_sizes = left.symbols.iter().map(|s| (s.section_address, s.size)).collect::<Vec<_>>();
|
||||
let right_sizes = right.symbols.iter().map(|s| (s.section_address, s.size)).collect::<Vec<_>>();
|
||||
let ops = capture_diff_slices_deadline(
|
||||
Algorithm::Patience,
|
||||
&left_sizes,
|
||||
&right_sizes,
|
||||
Some(deadline),
|
||||
);
|
||||
let match_percent = get_diff_ratio(&ops, left_sizes.len(), right_sizes.len()) * 100.0;
|
||||
Ok((
|
||||
ObjSectionDiff { symbols: vec![], data_diff: vec![], match_percent: Some(match_percent) },
|
||||
ObjSectionDiff { symbols: vec![], data_diff: vec![], match_percent: Some(match_percent) },
|
||||
))
|
||||
}
|
||||
|
||||
pub fn diff_bss_symbol(
|
||||
left_obj: &ObjInfo,
|
||||
right_obj: &ObjInfo,
|
||||
@@ -65,14 +41,16 @@ pub fn no_diff_symbol(_obj: &ObjInfo, symbol_ref: SymbolRef) -> ObjSymbolDiff {
|
||||
pub fn diff_data_section(
|
||||
left: &ObjSection,
|
||||
right: &ObjSection,
|
||||
left_section_diff: &ObjSectionDiff,
|
||||
right_section_diff: &ObjSectionDiff,
|
||||
) -> Result<(ObjSectionDiff, ObjSectionDiff)> {
|
||||
let deadline = Instant::now() + Duration::from_secs(5);
|
||||
let left_max = left.symbols.iter().map(|s| s.section_address + s.size).max().unwrap_or(0);
|
||||
let right_max = right.symbols.iter().map(|s| s.section_address + s.size).max().unwrap_or(0);
|
||||
let left_max =
|
||||
left.symbols.iter().map(|s| s.section_address + s.size).max().unwrap_or(0).min(left.size);
|
||||
let right_max =
|
||||
right.symbols.iter().map(|s| s.section_address + s.size).max().unwrap_or(0).min(right.size);
|
||||
let left_data = &left.data[..left_max as usize];
|
||||
let right_data = &right.data[..right_max as usize];
|
||||
let ops =
|
||||
capture_diff_slices_deadline(Algorithm::Patience, left_data, right_data, Some(deadline));
|
||||
let ops = capture_diff_slices_deadline(Algorithm::Patience, left_data, right_data, None);
|
||||
let match_percent = get_diff_ratio(&ops, left_data.len(), right_data.len()) * 100.0;
|
||||
|
||||
let mut left_diff = Vec::<ObjDataDiff>::new();
|
||||
@@ -143,18 +121,18 @@ pub fn diff_data_section(
|
||||
}
|
||||
}
|
||||
|
||||
Ok((
|
||||
ObjSectionDiff {
|
||||
symbols: vec![],
|
||||
data_diff: left_diff,
|
||||
match_percent: Some(match_percent),
|
||||
},
|
||||
ObjSectionDiff {
|
||||
symbols: vec![],
|
||||
data_diff: right_diff,
|
||||
match_percent: Some(match_percent),
|
||||
},
|
||||
))
|
||||
let (mut left_section_diff, mut right_section_diff) =
|
||||
diff_generic_section(left, right, left_section_diff, right_section_diff)?;
|
||||
left_section_diff.data_diff = left_diff;
|
||||
right_section_diff.data_diff = right_diff;
|
||||
// Use the highest match percent between two options:
|
||||
// - Left symbols matching right symbols by name
|
||||
// - Diff of the data itself
|
||||
if left_section_diff.match_percent.unwrap_or(-1.0) < match_percent {
|
||||
left_section_diff.match_percent = Some(match_percent);
|
||||
right_section_diff.match_percent = Some(match_percent);
|
||||
}
|
||||
Ok((left_section_diff, right_section_diff))
|
||||
}
|
||||
|
||||
pub fn diff_data_symbol(
|
||||
@@ -174,9 +152,7 @@ pub fn diff_data_symbol(
|
||||
let right_data = &right_section.data[right_symbol.section_address as usize
|
||||
..(right_symbol.section_address + right_symbol.size) as usize];
|
||||
|
||||
let deadline = Instant::now() + Duration::from_secs(5);
|
||||
let ops =
|
||||
capture_diff_slices_deadline(Algorithm::Patience, left_data, right_data, Some(deadline));
|
||||
let ops = capture_diff_slices_deadline(Algorithm::Patience, left_data, right_data, None);
|
||||
let match_percent = get_diff_ratio(&ops, left_data.len(), right_data.len()) * 100.0;
|
||||
|
||||
Ok((
|
||||
@@ -195,21 +171,50 @@ pub fn diff_data_symbol(
|
||||
))
|
||||
}
|
||||
|
||||
/// Compare the text sections of two object files.
|
||||
/// This essentially adds up the match percentage of each symbol in the text section.
|
||||
pub fn diff_text_section(
|
||||
/// Compares a section of two object files.
|
||||
/// This essentially adds up the match percentage of each symbol in the section.
|
||||
pub fn diff_generic_section(
|
||||
left: &ObjSection,
|
||||
_right: &ObjSection,
|
||||
left_diff: &ObjSectionDiff,
|
||||
_right_diff: &ObjSectionDiff,
|
||||
) -> Result<(ObjSectionDiff, ObjSectionDiff)> {
|
||||
let match_percent = left
|
||||
.symbols
|
||||
.iter()
|
||||
.zip(left_diff.symbols.iter())
|
||||
.map(|(s, d)| d.match_percent.unwrap_or(0.0) * s.size as f32)
|
||||
.sum::<f32>()
|
||||
/ left.size as f32;
|
||||
let match_percent = if left_diff.symbols.iter().all(|d| d.match_percent == Some(100.0)) {
|
||||
100.0 // Avoid fp precision issues
|
||||
} else {
|
||||
left.symbols
|
||||
.iter()
|
||||
.zip(left_diff.symbols.iter())
|
||||
.map(|(s, d)| d.match_percent.unwrap_or(0.0) * s.size as f32)
|
||||
.sum::<f32>()
|
||||
/ left.size as f32
|
||||
};
|
||||
Ok((
|
||||
ObjSectionDiff { symbols: vec![], data_diff: vec![], match_percent: Some(match_percent) },
|
||||
ObjSectionDiff { symbols: vec![], data_diff: vec![], match_percent: Some(match_percent) },
|
||||
))
|
||||
}
|
||||
|
||||
/// Compare the addresses and sizes of each symbol in the BSS sections.
|
||||
pub fn diff_bss_section(
|
||||
left: &ObjSection,
|
||||
right: &ObjSection,
|
||||
left_diff: &ObjSectionDiff,
|
||||
right_diff: &ObjSectionDiff,
|
||||
) -> Result<(ObjSectionDiff, ObjSectionDiff)> {
|
||||
let left_sizes = left.symbols.iter().map(|s| (s.section_address, s.size)).collect::<Vec<_>>();
|
||||
let right_sizes = right.symbols.iter().map(|s| (s.section_address, s.size)).collect::<Vec<_>>();
|
||||
let ops = capture_diff_slices_deadline(Algorithm::Patience, &left_sizes, &right_sizes, None);
|
||||
let mut match_percent = get_diff_ratio(&ops, left_sizes.len(), right_sizes.len()) * 100.0;
|
||||
|
||||
// Use the highest match percent between two options:
|
||||
// - Left symbols matching right symbols by name
|
||||
// - Diff of the addresses and sizes of each symbol
|
||||
let (generic_diff, _) = diff_generic_section(left, right, left_diff, right_diff)?;
|
||||
if generic_diff.match_percent.unwrap_or(-1.0) > match_percent {
|
||||
match_percent = generic_diff.match_percent.unwrap();
|
||||
}
|
||||
|
||||
Ok((
|
||||
ObjSectionDiff { symbols: vec![], data_diff: vec![], match_percent: Some(match_percent) },
|
||||
ObjSectionDiff { symbols: vec![], data_diff: vec![], match_percent: Some(match_percent) },
|
||||
|
||||
@@ -12,7 +12,7 @@ pub enum DiffText<'a> {
|
||||
/// Colored text
|
||||
BasicColor(&'a str, usize),
|
||||
/// Line number
|
||||
Line(usize),
|
||||
Line(u32),
|
||||
/// Instruction address
|
||||
Address(u64),
|
||||
/// Instruction mnemonic
|
||||
@@ -20,7 +20,7 @@ pub enum DiffText<'a> {
|
||||
/// Instruction argument
|
||||
Argument(&'a ObjInsArgValue, Option<&'a ObjInsArgDiff>),
|
||||
/// Branch destination
|
||||
BranchDest(u64),
|
||||
BranchDest(u64, Option<&'a ObjInsArgDiff>),
|
||||
/// Symbol name
|
||||
Symbol(&'a ObjSymbol),
|
||||
/// Number of spaces
|
||||
@@ -49,7 +49,7 @@ pub fn display_diff<E>(
|
||||
return Ok(());
|
||||
};
|
||||
if let Some(line) = ins.line {
|
||||
cb(DiffText::Line(line as usize))?;
|
||||
cb(DiffText::Line(line))?;
|
||||
}
|
||||
cb(DiffText::Address(ins.address - base_addr))?;
|
||||
if let Some(branch) = &ins_diff.branch_from {
|
||||
@@ -62,12 +62,12 @@ pub fn display_diff<E>(
|
||||
if i == 0 {
|
||||
cb(DiffText::Spacing(1))?;
|
||||
}
|
||||
let diff = ins_diff.arg_diff.get(i).and_then(|o| o.as_ref());
|
||||
match arg {
|
||||
ObjInsArg::PlainText(s) => {
|
||||
cb(DiffText::Basic(s))?;
|
||||
}
|
||||
ObjInsArg::Arg(v) => {
|
||||
let diff = ins_diff.arg_diff.get(i).and_then(|o| o.as_ref());
|
||||
cb(DiffText::Argument(v, diff))?;
|
||||
}
|
||||
ObjInsArg::Reloc => {
|
||||
@@ -75,7 +75,7 @@ pub fn display_diff<E>(
|
||||
}
|
||||
ObjInsArg::BranchDest(dest) => {
|
||||
if let Some(dest) = dest.checked_sub(base_addr) {
|
||||
cb(DiffText::BranchDest(dest))?;
|
||||
cb(DiffText::BranchDest(dest, diff))?;
|
||||
} else {
|
||||
cb(DiffText::Basic("<unknown>"))?;
|
||||
}
|
||||
@@ -107,7 +107,9 @@ impl PartialEq<DiffText<'_>> for HighlightKind {
|
||||
(HighlightKind::Opcode(a), DiffText::Opcode(_, b)) => a == b,
|
||||
(HighlightKind::Arg(a), DiffText::Argument(b, _)) => a.loose_eq(b),
|
||||
(HighlightKind::Symbol(a), DiffText::Symbol(b)) => a == &b.name,
|
||||
(HighlightKind::Address(a), DiffText::Address(b) | DiffText::BranchDest(b)) => a == b,
|
||||
(HighlightKind::Address(a), DiffText::Address(b) | DiffText::BranchDest(b, _)) => {
|
||||
a == b
|
||||
}
|
||||
_ => false,
|
||||
}
|
||||
}
|
||||
@@ -123,7 +125,7 @@ impl From<DiffText<'_>> for HighlightKind {
|
||||
DiffText::Opcode(_, op) => HighlightKind::Opcode(op),
|
||||
DiffText::Argument(arg, _) => HighlightKind::Arg(arg.clone()),
|
||||
DiffText::Symbol(sym) => HighlightKind::Symbol(sym.name.to_string()),
|
||||
DiffText::Address(addr) | DiffText::BranchDest(addr) => HighlightKind::Address(addr),
|
||||
DiffText::Address(addr) | DiffText::BranchDest(addr, _) => HighlightKind::Address(addr),
|
||||
_ => HighlightKind::None,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,38 +4,196 @@ use anyhow::Result;
|
||||
|
||||
use crate::{
|
||||
diff::{
|
||||
code::{diff_code, no_diff_code},
|
||||
code::{diff_code, no_diff_code, process_code_symbol},
|
||||
data::{
|
||||
diff_bss_section, diff_bss_symbol, diff_data_section, diff_data_symbol,
|
||||
diff_text_section, no_diff_symbol,
|
||||
diff_generic_section, no_diff_symbol,
|
||||
},
|
||||
},
|
||||
obj::{ObjInfo, ObjIns, ObjSection, ObjSectionKind, ObjSymbol, SymbolRef},
|
||||
};
|
||||
|
||||
mod code;
|
||||
mod data;
|
||||
pub mod code;
|
||||
pub mod data;
|
||||
pub mod display;
|
||||
|
||||
#[derive(Debug, Copy, Clone, Default, Eq, PartialEq, serde::Deserialize, serde::Serialize)]
|
||||
#[derive(
|
||||
Debug,
|
||||
Copy,
|
||||
Clone,
|
||||
Default,
|
||||
Eq,
|
||||
PartialEq,
|
||||
serde::Deserialize,
|
||||
serde::Serialize,
|
||||
strum::VariantArray,
|
||||
strum::EnumMessage,
|
||||
tsify_next::Tsify,
|
||||
)]
|
||||
pub enum X86Formatter {
|
||||
#[default]
|
||||
#[strum(message = "Intel (default)")]
|
||||
Intel,
|
||||
#[strum(message = "AT&T")]
|
||||
Gas,
|
||||
#[strum(message = "NASM")]
|
||||
Nasm,
|
||||
#[strum(message = "MASM")]
|
||||
Masm,
|
||||
}
|
||||
|
||||
#[derive(
|
||||
Debug,
|
||||
Copy,
|
||||
Clone,
|
||||
Default,
|
||||
Eq,
|
||||
PartialEq,
|
||||
serde::Deserialize,
|
||||
serde::Serialize,
|
||||
strum::VariantArray,
|
||||
strum::EnumMessage,
|
||||
tsify_next::Tsify,
|
||||
)]
|
||||
pub enum MipsAbi {
|
||||
#[default]
|
||||
#[strum(message = "Auto (default)")]
|
||||
Auto,
|
||||
#[strum(message = "O32")]
|
||||
O32,
|
||||
#[strum(message = "N32")]
|
||||
N32,
|
||||
#[strum(message = "N64")]
|
||||
N64,
|
||||
}
|
||||
|
||||
#[derive(
|
||||
Debug,
|
||||
Copy,
|
||||
Clone,
|
||||
Default,
|
||||
Eq,
|
||||
PartialEq,
|
||||
serde::Deserialize,
|
||||
serde::Serialize,
|
||||
strum::VariantArray,
|
||||
strum::EnumMessage,
|
||||
tsify_next::Tsify,
|
||||
)]
|
||||
pub enum MipsInstrCategory {
|
||||
#[default]
|
||||
#[strum(message = "Auto (default)")]
|
||||
Auto,
|
||||
#[strum(message = "CPU")]
|
||||
Cpu,
|
||||
#[strum(message = "RSP (N64)")]
|
||||
Rsp,
|
||||
#[strum(message = "R3000 GTE (PS1)")]
|
||||
R3000Gte,
|
||||
#[strum(message = "R4000 ALLEGREX (PSP)")]
|
||||
R4000Allegrex,
|
||||
#[strum(message = "R5900 EE (PS2)")]
|
||||
R5900,
|
||||
}
|
||||
|
||||
#[derive(
|
||||
Debug,
|
||||
Copy,
|
||||
Clone,
|
||||
Default,
|
||||
Eq,
|
||||
PartialEq,
|
||||
serde::Deserialize,
|
||||
serde::Serialize,
|
||||
strum::VariantArray,
|
||||
strum::EnumMessage,
|
||||
tsify_next::Tsify,
|
||||
)]
|
||||
pub enum ArmArchVersion {
|
||||
#[default]
|
||||
#[strum(message = "Auto (default)")]
|
||||
Auto,
|
||||
#[strum(message = "ARMv4T (GBA)")]
|
||||
V4T,
|
||||
#[strum(message = "ARMv5TE (DS)")]
|
||||
V5TE,
|
||||
#[strum(message = "ARMv6K (3DS)")]
|
||||
V6K,
|
||||
}
|
||||
|
||||
#[derive(
|
||||
Debug,
|
||||
Copy,
|
||||
Clone,
|
||||
Default,
|
||||
Eq,
|
||||
PartialEq,
|
||||
serde::Deserialize,
|
||||
serde::Serialize,
|
||||
strum::VariantArray,
|
||||
strum::EnumMessage,
|
||||
tsify_next::Tsify,
|
||||
)]
|
||||
pub enum ArmR9Usage {
|
||||
#[default]
|
||||
#[strum(
|
||||
message = "R9 or V6 (default)",
|
||||
detailed_message = "Use R9 as a general-purpose register."
|
||||
)]
|
||||
GeneralPurpose,
|
||||
#[strum(
|
||||
message = "SB (static base)",
|
||||
detailed_message = "Used for position-independent data (PID)."
|
||||
)]
|
||||
Sb,
|
||||
#[strum(message = "TR (TLS register)", detailed_message = "Used for thread-local storage.")]
|
||||
Tr,
|
||||
}
|
||||
|
||||
#[inline]
|
||||
const fn default_true() -> bool { true }
|
||||
|
||||
#[derive(Debug, Clone, Default, Eq, PartialEq, serde::Deserialize, serde::Serialize)]
|
||||
#[derive(Debug, Clone, Eq, PartialEq, serde::Deserialize, serde::Serialize, tsify_next::Tsify)]
|
||||
#[tsify(from_wasm_abi)]
|
||||
#[serde(default)]
|
||||
pub struct DiffObjConfig {
|
||||
pub relax_reloc_diffs: bool,
|
||||
#[serde(default = "default_true")]
|
||||
pub space_between_args: bool,
|
||||
pub combine_data_sections: bool,
|
||||
// x86
|
||||
pub x86_formatter: X86Formatter,
|
||||
// MIPS
|
||||
pub mips_abi: MipsAbi,
|
||||
pub mips_instr_category: MipsInstrCategory,
|
||||
// ARM
|
||||
pub arm_arch_version: ArmArchVersion,
|
||||
pub arm_unified_syntax: bool,
|
||||
pub arm_av_registers: bool,
|
||||
pub arm_r9_usage: ArmR9Usage,
|
||||
pub arm_sl_usage: bool,
|
||||
pub arm_fp_usage: bool,
|
||||
pub arm_ip_usage: bool,
|
||||
}
|
||||
|
||||
impl Default for DiffObjConfig {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
relax_reloc_diffs: false,
|
||||
space_between_args: true,
|
||||
combine_data_sections: false,
|
||||
x86_formatter: Default::default(),
|
||||
mips_abi: Default::default(),
|
||||
mips_instr_category: Default::default(),
|
||||
arm_arch_version: Default::default(),
|
||||
arm_unified_syntax: true,
|
||||
arm_av_registers: false,
|
||||
arm_r9_usage: Default::default(),
|
||||
arm_sl_usage: false,
|
||||
arm_fp_usage: false,
|
||||
arm_ip_usage: false,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl DiffObjConfig {
|
||||
@@ -238,9 +396,11 @@ pub fn diff_objs(
|
||||
let (right_obj, right_out) = right.as_mut().unwrap();
|
||||
match section_kind {
|
||||
ObjSectionKind::Code => {
|
||||
let left_code = process_code_symbol(left_obj, left_symbol_ref, config)?;
|
||||
let right_code = process_code_symbol(right_obj, right_symbol_ref, config)?;
|
||||
let (left_diff, right_diff) = diff_code(
|
||||
left_obj,
|
||||
right_obj,
|
||||
&left_code,
|
||||
&right_code,
|
||||
left_symbol_ref,
|
||||
right_symbol_ref,
|
||||
config,
|
||||
@@ -250,9 +410,10 @@ pub fn diff_objs(
|
||||
|
||||
if let Some(prev_symbol_ref) = prev_symbol_ref {
|
||||
let (prev_obj, prev_out) = prev.as_mut().unwrap();
|
||||
let prev_code = process_code_symbol(prev_obj, prev_symbol_ref, config)?;
|
||||
let (_, prev_diff) = diff_code(
|
||||
right_obj,
|
||||
prev_obj,
|
||||
&right_code,
|
||||
&prev_code,
|
||||
right_symbol_ref,
|
||||
prev_symbol_ref,
|
||||
config,
|
||||
@@ -286,8 +447,9 @@ pub fn diff_objs(
|
||||
let (left_obj, left_out) = left.as_mut().unwrap();
|
||||
match section_kind {
|
||||
ObjSectionKind::Code => {
|
||||
let code = process_code_symbol(left_obj, left_symbol_ref, config)?;
|
||||
*left_out.symbol_diff_mut(left_symbol_ref) =
|
||||
no_diff_code(left_obj, left_symbol_ref, config)?;
|
||||
no_diff_code(&code, left_symbol_ref)?;
|
||||
}
|
||||
ObjSectionKind::Data | ObjSectionKind::Bss => {
|
||||
*left_out.symbol_diff_mut(left_symbol_ref) =
|
||||
@@ -299,8 +461,9 @@ pub fn diff_objs(
|
||||
let (right_obj, right_out) = right.as_mut().unwrap();
|
||||
match section_kind {
|
||||
ObjSectionKind::Code => {
|
||||
let code = process_code_symbol(right_obj, right_symbol_ref, config)?;
|
||||
*right_out.symbol_diff_mut(right_symbol_ref) =
|
||||
no_diff_code(right_obj, right_symbol_ref, config)?;
|
||||
no_diff_code(&code, right_symbol_ref)?;
|
||||
}
|
||||
ObjSectionKind::Data | ObjSectionKind::Bss => {
|
||||
*right_out.symbol_diff_mut(right_symbol_ref) =
|
||||
@@ -329,7 +492,7 @@ pub fn diff_objs(
|
||||
ObjSectionKind::Code => {
|
||||
let left_section_diff = left_out.section_diff(left_section_idx);
|
||||
let right_section_diff = right_out.section_diff(right_section_idx);
|
||||
let (left_diff, right_diff) = diff_text_section(
|
||||
let (left_diff, right_diff) = diff_generic_section(
|
||||
left_section,
|
||||
right_section,
|
||||
left_section_diff,
|
||||
@@ -339,12 +502,26 @@ pub fn diff_objs(
|
||||
right_out.section_diff_mut(right_section_idx).merge(right_diff);
|
||||
}
|
||||
ObjSectionKind::Data => {
|
||||
let (left_diff, right_diff) = diff_data_section(left_section, right_section)?;
|
||||
let left_section_diff = left_out.section_diff(left_section_idx);
|
||||
let right_section_diff = right_out.section_diff(right_section_idx);
|
||||
let (left_diff, right_diff) = diff_data_section(
|
||||
left_section,
|
||||
right_section,
|
||||
left_section_diff,
|
||||
right_section_diff,
|
||||
)?;
|
||||
left_out.section_diff_mut(left_section_idx).merge(left_diff);
|
||||
right_out.section_diff_mut(right_section_idx).merge(right_diff);
|
||||
}
|
||||
ObjSectionKind::Bss => {
|
||||
let (left_diff, right_diff) = diff_bss_section(left_section, right_section)?;
|
||||
let left_section_diff = left_out.section_diff(left_section_idx);
|
||||
let right_section_diff = right_out.section_diff(right_section_idx);
|
||||
let (left_diff, right_diff) = diff_bss_section(
|
||||
left_section,
|
||||
right_section,
|
||||
left_section_diff,
|
||||
right_section_diff,
|
||||
)?;
|
||||
left_out.section_diff_mut(left_section_idx).merge(left_diff);
|
||||
right_out.section_diff_mut(right_section_idx).merge(right_diff);
|
||||
}
|
||||
@@ -387,8 +564,8 @@ fn matching_symbols(
|
||||
for (symbol_idx, symbol) in section.symbols.iter().enumerate() {
|
||||
let symbol_match = SymbolMatch {
|
||||
left: Some(SymbolRef { section_idx, symbol_idx }),
|
||||
right: find_symbol(right, symbol, section),
|
||||
prev: find_symbol(prev, symbol, section),
|
||||
right: find_symbol(right, symbol, section, Some(&right_used)),
|
||||
prev: find_symbol(prev, symbol, section, None),
|
||||
section_kind: section.kind,
|
||||
};
|
||||
matches.push(symbol_match);
|
||||
@@ -420,7 +597,7 @@ fn matching_symbols(
|
||||
matches.push(SymbolMatch {
|
||||
left: None,
|
||||
right: Some(symbol_ref),
|
||||
prev: find_symbol(prev, symbol, section),
|
||||
prev: find_symbol(prev, symbol, section, None),
|
||||
section_kind: section.kind,
|
||||
});
|
||||
}
|
||||
@@ -441,10 +618,25 @@ fn matching_symbols(
|
||||
Ok(matches)
|
||||
}
|
||||
|
||||
fn unmatched_symbols<'section, 'used>(
|
||||
section: &'section ObjSection,
|
||||
section_idx: usize,
|
||||
used: Option<&'used HashSet<SymbolRef>>,
|
||||
) -> impl Iterator<Item = (usize, &'section ObjSymbol)> + 'used
|
||||
where
|
||||
'section: 'used,
|
||||
{
|
||||
section.symbols.iter().enumerate().filter(move |&(symbol_idx, _)| {
|
||||
// Skip symbols that have already been matched
|
||||
!used.map(|u| u.contains(&SymbolRef { section_idx, symbol_idx })).unwrap_or(false)
|
||||
})
|
||||
}
|
||||
|
||||
fn find_symbol(
|
||||
obj: Option<&ObjInfo>,
|
||||
in_symbol: &ObjSymbol,
|
||||
in_section: &ObjSection,
|
||||
used: Option<&HashSet<SymbolRef>>,
|
||||
) -> Option<SymbolRef> {
|
||||
let obj = obj?;
|
||||
// Try to find an exact name match
|
||||
@@ -452,8 +644,8 @@ fn find_symbol(
|
||||
if section.kind != in_section.kind {
|
||||
continue;
|
||||
}
|
||||
if let Some(symbol_idx) =
|
||||
section.symbols.iter().position(|symbol| symbol.name == in_symbol.name)
|
||||
if let Some((symbol_idx, _)) = unmatched_symbols(section, section_idx, used)
|
||||
.find(|(_, symbol)| symbol.name == in_symbol.name)
|
||||
{
|
||||
return Some(SymbolRef { section_idx, symbol_idx });
|
||||
}
|
||||
@@ -466,9 +658,33 @@ fn find_symbol(
|
||||
if let Some((section_idx, section)) =
|
||||
obj.sections.iter().enumerate().find(|(_, s)| s.name == in_section.name)
|
||||
{
|
||||
if let Some(symbol_idx) = section.symbols.iter().position(|symbol| {
|
||||
symbol.address == in_symbol.address && symbol.name.starts_with('@')
|
||||
}) {
|
||||
if let Some((symbol_idx, _)) =
|
||||
unmatched_symbols(section, section_idx, used).find(|(_, symbol)| {
|
||||
symbol.address == in_symbol.address && symbol.name.starts_with('@')
|
||||
})
|
||||
{
|
||||
return Some(SymbolRef { section_idx, symbol_idx });
|
||||
}
|
||||
}
|
||||
}
|
||||
// Match Metrowerks symbol$1234 against symbol$2345
|
||||
if let Some((prefix, suffix)) = in_symbol.name.split_once('$') {
|
||||
if !suffix.chars().all(char::is_numeric) {
|
||||
return None;
|
||||
}
|
||||
for (section_idx, section) in obj.sections.iter().enumerate() {
|
||||
if section.kind != in_section.kind {
|
||||
continue;
|
||||
}
|
||||
if let Some((symbol_idx, _)) =
|
||||
unmatched_symbols(section, section_idx, used).find(|&(_, symbol)| {
|
||||
if let Some((p, s)) = symbol.name.split_once('$') {
|
||||
prefix == p && s.chars().all(char::is_numeric)
|
||||
} else {
|
||||
false
|
||||
}
|
||||
})
|
||||
{
|
||||
return Some(SymbolRef { section_idx, symbol_idx });
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
#[cfg(feature = "any-arch")]
|
||||
pub mod arch;
|
||||
#[cfg(feature = "bindings")]
|
||||
pub mod bindings;
|
||||
#[cfg(feature = "config")]
|
||||
pub mod config;
|
||||
#[cfg(feature = "any-arch")]
|
||||
pub mod diff;
|
||||
#[cfg(feature = "any-arch")]
|
||||
pub mod obj;
|
||||
pub mod util;
|
||||
|
||||
#[cfg(not(feature = "any-arch"))]
|
||||
compile_error!("At least one architecture feature must be enabled.");
|
||||
|
||||
@@ -3,6 +3,7 @@ pub mod split_meta;
|
||||
|
||||
use std::{borrow::Cow, collections::BTreeMap, fmt, path::PathBuf};
|
||||
|
||||
use cwextab::*;
|
||||
use filetime::FileTime;
|
||||
use flagset::{flags, FlagSet};
|
||||
use object::RelocationFlags;
|
||||
@@ -40,7 +41,7 @@ pub struct ObjSection {
|
||||
pub relocations: Vec<ObjReloc>,
|
||||
pub virtual_address: Option<u64>,
|
||||
/// Line number info (.line or .debug_line section)
|
||||
pub line_info: BTreeMap<u64, u64>,
|
||||
pub line_info: BTreeMap<u64, u32>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Eq, PartialEq)]
|
||||
@@ -102,7 +103,7 @@ pub struct ObjIns {
|
||||
pub reloc: Option<ObjReloc>,
|
||||
pub branch_dest: Option<u64>,
|
||||
/// Line number
|
||||
pub line: Option<u64>,
|
||||
pub line: Option<u32>,
|
||||
/// Formatted instruction
|
||||
pub formatted: String,
|
||||
/// Original (unsimplified) instruction
|
||||
@@ -113,6 +114,9 @@ pub struct ObjIns {
|
||||
pub struct ObjSymbol {
|
||||
pub name: String,
|
||||
pub demangled_name: Option<String>,
|
||||
pub has_extab: bool,
|
||||
pub extab_name: Option<String>,
|
||||
pub extabindex_name: Option<String>,
|
||||
pub address: u64,
|
||||
pub section_address: u64,
|
||||
pub size: u64,
|
||||
@@ -123,13 +127,22 @@ pub struct ObjSymbol {
|
||||
pub virtual_address: Option<u64>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct ObjExtab {
|
||||
pub func: ObjSymbol,
|
||||
pub data: ExceptionTableData,
|
||||
pub dtors: Vec<ObjSymbol>,
|
||||
}
|
||||
|
||||
pub struct ObjInfo {
|
||||
pub arch: Box<dyn ObjArch>,
|
||||
pub path: PathBuf,
|
||||
pub timestamp: FileTime,
|
||||
pub path: Option<PathBuf>,
|
||||
pub timestamp: Option<FileTime>,
|
||||
pub sections: Vec<ObjSection>,
|
||||
/// Common BSS symbols
|
||||
pub common: Vec<ObjSymbol>,
|
||||
/// Exception tables
|
||||
pub extab: Option<Vec<ObjExtab>>,
|
||||
/// Split object metadata (.note.split section)
|
||||
pub split_meta: Option<SplitMeta>,
|
||||
}
|
||||
|
||||
@@ -1,20 +1,26 @@
|
||||
use std::{fs, io::Cursor, path::Path};
|
||||
use std::{collections::HashSet, fs, io::Cursor, mem::size_of, path::Path};
|
||||
|
||||
use anyhow::{anyhow, bail, ensure, Context, Result};
|
||||
use byteorder::{BigEndian, ReadBytesExt};
|
||||
use cwextab::decode_extab;
|
||||
use filetime::FileTime;
|
||||
use flagset::Flags;
|
||||
use object::{
|
||||
BinaryFormat, File, Object, ObjectSection, ObjectSymbol, RelocationTarget, SectionIndex,
|
||||
SectionKind, Symbol, SymbolKind, SymbolScope, SymbolSection,
|
||||
endian::LittleEndian as LE,
|
||||
pe::{ImageAuxSymbolFunctionBeginEnd, ImageLinenumber},
|
||||
read::coff::{CoffFile, CoffHeader, ImageSymbol},
|
||||
Architecture, BinaryFormat, File, Object, ObjectSection, ObjectSymbol, RelocationTarget,
|
||||
SectionIndex, SectionKind, Symbol, SymbolIndex, SymbolKind, SymbolScope, SymbolSection,
|
||||
};
|
||||
|
||||
use crate::{
|
||||
arch::{new_arch, ObjArch},
|
||||
diff::DiffObjConfig,
|
||||
obj::{
|
||||
split_meta::{SplitMeta, SPLITMETA_SECTION},
|
||||
ObjInfo, ObjReloc, ObjSection, ObjSectionKind, ObjSymbol, ObjSymbolFlagSet, ObjSymbolFlags,
|
||||
ObjExtab, ObjInfo, ObjReloc, ObjSection, ObjSectionKind, ObjSymbol, ObjSymbolFlagSet,
|
||||
ObjSymbolFlags,
|
||||
},
|
||||
util::{read_u16, read_u32},
|
||||
};
|
||||
|
||||
fn to_obj_section_kind(kind: SectionKind) -> Option<ObjSectionKind> {
|
||||
@@ -54,12 +60,13 @@ fn to_obj_symbol(
|
||||
if obj_file.format() == BinaryFormat::Elf && symbol.scope() == SymbolScope::Linkage {
|
||||
flags = ObjSymbolFlagSet(flags.0 | ObjSymbolFlags::Hidden);
|
||||
}
|
||||
let address = arch.symbol_address(symbol);
|
||||
let section_address = if let Some(section) =
|
||||
symbol.section_index().and_then(|idx| obj_file.section_by_index(idx).ok())
|
||||
{
|
||||
symbol.address() - section.address()
|
||||
address - section.address()
|
||||
} else {
|
||||
symbol.address()
|
||||
address
|
||||
};
|
||||
let demangled_name = arch.demangle(name);
|
||||
// Find the virtual address for the symbol if available
|
||||
@@ -69,7 +76,10 @@ fn to_obj_symbol(
|
||||
Ok(ObjSymbol {
|
||||
name: name.to_string(),
|
||||
demangled_name,
|
||||
address: symbol.address(),
|
||||
has_extab: false,
|
||||
extab_name: None,
|
||||
extabindex_name: None,
|
||||
address,
|
||||
section_address,
|
||||
size: symbol.size(),
|
||||
size_known: symbol.size() != 0,
|
||||
@@ -142,7 +152,7 @@ fn symbols_by_section(
|
||||
}
|
||||
}
|
||||
}
|
||||
result.sort_by_key(|v| v.address);
|
||||
result.sort_by(|a, b| a.address.cmp(&b.address).then(a.size.cmp(&b.size)));
|
||||
let mut iter = result.iter_mut().peekable();
|
||||
while let Some(symbol) = iter.next() {
|
||||
if symbol.size == 0 {
|
||||
@@ -153,6 +163,23 @@ fn symbols_by_section(
|
||||
}
|
||||
}
|
||||
}
|
||||
if result.is_empty() {
|
||||
// Dummy symbol for empty sections
|
||||
result.push(ObjSymbol {
|
||||
name: format!("[{}]", section.name),
|
||||
demangled_name: None,
|
||||
has_extab: false,
|
||||
extab_name: None,
|
||||
extabindex_name: None,
|
||||
address: 0,
|
||||
section_address: 0,
|
||||
size: section.size,
|
||||
size_known: true,
|
||||
flags: Default::default(),
|
||||
addend: 0,
|
||||
virtual_address: None,
|
||||
});
|
||||
}
|
||||
Ok(result)
|
||||
}
|
||||
|
||||
@@ -168,6 +195,111 @@ fn common_symbols(
|
||||
.collect::<Result<Vec<ObjSymbol>>>()
|
||||
}
|
||||
|
||||
fn section_by_name<'a>(sections: &'a mut [ObjSection], name: &str) -> Option<&'a mut ObjSection> {
|
||||
sections.iter_mut().find(|section| section.name == name)
|
||||
}
|
||||
|
||||
fn exception_tables(
|
||||
sections: &mut [ObjSection],
|
||||
obj_file: &File<'_>,
|
||||
) -> Result<Option<Vec<ObjExtab>>> {
|
||||
//PowerPC only
|
||||
if obj_file.architecture() != Architecture::PowerPc {
|
||||
return Ok(None);
|
||||
}
|
||||
|
||||
//Find the extab/extabindex sections
|
||||
let extab_section = match section_by_name(sections, "extab") {
|
||||
Some(section) => section.clone(),
|
||||
None => {
|
||||
return Ok(None);
|
||||
}
|
||||
};
|
||||
let extabindex_section = match section_by_name(sections, "extabindex") {
|
||||
Some(section) => section.clone(),
|
||||
None => {
|
||||
return Ok(None);
|
||||
}
|
||||
};
|
||||
let text_section = match section_by_name(sections, ".text") {
|
||||
Some(section) => section,
|
||||
None => bail!(".text section is somehow missing, this should not happen"),
|
||||
};
|
||||
|
||||
let mut result: Vec<ObjExtab> = vec![];
|
||||
let extab_symbol_count = extab_section.symbols.len();
|
||||
let extabindex_symbol_count = extabindex_section.symbols.len();
|
||||
let extab_reloc_count = extab_section.relocations.len();
|
||||
let table_count = extab_symbol_count;
|
||||
let mut extab_reloc_index: usize = 0;
|
||||
|
||||
//Make sure that the number of symbols in the extab/extabindex section matches. If not, exit early
|
||||
if extab_symbol_count != extabindex_symbol_count {
|
||||
bail!("Extab/Extabindex symbol counts do not match");
|
||||
}
|
||||
|
||||
//Convert the extab/extabindex section data
|
||||
|
||||
//Go through each extabindex entry
|
||||
for i in 0..table_count {
|
||||
let extabindex = &extabindex_section.symbols[i];
|
||||
|
||||
/* Get the function symbol and extab symbol from the extabindex relocations array. Each extabindex
|
||||
entry has two relocations (the first for the function, the second for the extab entry) */
|
||||
let extab_func = extabindex_section.relocations[i * 2].target.clone();
|
||||
let extab = &extabindex_section.relocations[(i * 2) + 1].target;
|
||||
|
||||
let extab_start_addr = extab.address;
|
||||
let extab_end_addr = extab_start_addr + extab.size;
|
||||
|
||||
//Find the function in the text section, and set the has extab flag
|
||||
for i in 0..text_section.symbols.len() {
|
||||
let func = &mut text_section.symbols[i];
|
||||
if func.name == extab_func.name {
|
||||
func.has_extab = true;
|
||||
func.extab_name = Some(extab.name.clone());
|
||||
func.extabindex_name = Some(extabindex.name.clone());
|
||||
}
|
||||
}
|
||||
|
||||
/* Iterate through the list of extab relocations, continuing until we hit a relocation
|
||||
that isn't within the current extab symbol. Get the target dtor function symbol from
|
||||
each relocation used, and add them to the list. */
|
||||
let mut dtors: Vec<ObjSymbol> = vec![];
|
||||
|
||||
while extab_reloc_index < extab_reloc_count {
|
||||
let extab_reloc = &extab_section.relocations[extab_reloc_index];
|
||||
//If the current entry is past the current extab table, stop here
|
||||
if extab_reloc.address >= extab_end_addr {
|
||||
break;
|
||||
}
|
||||
|
||||
//Otherwise, the current relocation is used by the current table
|
||||
dtors.push(extab_reloc.target.clone());
|
||||
//Go to the next entry
|
||||
extab_reloc_index += 1;
|
||||
}
|
||||
|
||||
//Decode the extab data
|
||||
let start_index = extab_start_addr as usize;
|
||||
let end_index = extab_end_addr as usize;
|
||||
let extab_data = extab_section.data[start_index..end_index].try_into().unwrap();
|
||||
let data = match decode_extab(extab_data) {
|
||||
Some(decoded_data) => decoded_data,
|
||||
None => {
|
||||
log::warn!("Exception table decoding failed for function {}", extab_func.name);
|
||||
return Ok(None);
|
||||
}
|
||||
};
|
||||
|
||||
//Add the new entry to the list
|
||||
let entry = ObjExtab { func: extab_func, data, dtors };
|
||||
result.push(entry);
|
||||
}
|
||||
|
||||
Ok(Some(result))
|
||||
}
|
||||
|
||||
fn find_section_symbol(
|
||||
arch: &dyn ObjArch,
|
||||
obj_file: &File<'_>,
|
||||
@@ -203,6 +335,9 @@ fn find_section_symbol(
|
||||
Ok(ObjSymbol {
|
||||
name: name.to_string(),
|
||||
demangled_name: None,
|
||||
has_extab: false,
|
||||
extab_name: None,
|
||||
extabindex_name: None,
|
||||
address: offset,
|
||||
section_address: address - section.address(),
|
||||
size: 0,
|
||||
@@ -249,7 +384,7 @@ fn relocations_by_section(
|
||||
_ => None,
|
||||
};
|
||||
let addend = if reloc.has_implicit_addend() {
|
||||
arch.implcit_addend(section, address, &reloc)?
|
||||
arch.implcit_addend(obj_file, section, address, &reloc)?
|
||||
} else {
|
||||
reloc.addend()
|
||||
};
|
||||
@@ -269,7 +404,7 @@ fn relocations_by_section(
|
||||
Ok(relocations)
|
||||
}
|
||||
|
||||
fn line_info(obj_file: &File<'_>, sections: &mut [ObjSection]) -> Result<()> {
|
||||
fn line_info(obj_file: &File<'_>, sections: &mut [ObjSection], obj_data: &[u8]) -> Result<()> {
|
||||
// DWARF 1.1
|
||||
if let Some(section) = obj_file.section_by_name(".line") {
|
||||
let data = section.uncompressed_data()?;
|
||||
@@ -283,8 +418,8 @@ fn line_info(obj_file: &File<'_>, sections: &mut [ObjSection]) -> Result<()> {
|
||||
.index()
|
||||
.0;
|
||||
let start = reader.position();
|
||||
let size = reader.read_u32::<BigEndian>()?;
|
||||
let base_address = reader.read_u32::<BigEndian>()? as u64;
|
||||
let size = read_u32(obj_file, &mut reader)?;
|
||||
let base_address = read_u32(obj_file, &mut reader)? as u64;
|
||||
let Some(out_section) =
|
||||
sections.iter_mut().find(|s| s.orig_index == text_section_index)
|
||||
else {
|
||||
@@ -294,12 +429,12 @@ fn line_info(obj_file: &File<'_>, sections: &mut [ObjSection]) -> Result<()> {
|
||||
};
|
||||
let end = start + size as u64;
|
||||
while reader.position() < end {
|
||||
let line_number = reader.read_u32::<BigEndian>()? as u64;
|
||||
let statement_pos = reader.read_u16::<BigEndian>()?;
|
||||
let line_number = read_u32(obj_file, &mut reader)?;
|
||||
let statement_pos = read_u16(obj_file, &mut reader)?;
|
||||
if statement_pos != 0xFFFF {
|
||||
log::warn!("Unhandled statement pos {}", statement_pos);
|
||||
}
|
||||
let address_delta = reader.read_u32::<BigEndian>()? as u64;
|
||||
let address_delta = read_u32(obj_file, &mut reader)? as u64;
|
||||
out_section.line_info.insert(base_address + address_delta, line_number);
|
||||
log::debug!("Line: {:#x} -> {}", base_address + address_delta, line_number);
|
||||
}
|
||||
@@ -328,20 +463,15 @@ fn line_info(obj_file: &File<'_>, sections: &mut [ObjSection]) -> Result<()> {
|
||||
if let Some(program) = unit.line_program.clone() {
|
||||
let mut text_sections =
|
||||
obj_file.sections().filter(|s| s.kind() == SectionKind::Text);
|
||||
let section_index = text_sections
|
||||
.next()
|
||||
.ok_or_else(|| anyhow!("Next text section not found for line info"))?
|
||||
.index()
|
||||
.0;
|
||||
let mut lines = sections
|
||||
.iter_mut()
|
||||
.find(|s| s.orig_index == section_index)
|
||||
.map(|s| &mut s.line_info);
|
||||
let section_index = text_sections.next().map(|s| s.index().0);
|
||||
let mut lines = section_index.map(|index| {
|
||||
&mut sections.iter_mut().find(|s| s.orig_index == index).unwrap().line_info
|
||||
});
|
||||
|
||||
let mut rows = program.rows();
|
||||
while let Some((_header, row)) = rows.next_row()? {
|
||||
if let (Some(line), Some(lines)) = (row.line(), &mut lines) {
|
||||
lines.insert(row.address(), line.get());
|
||||
lines.insert(row.address(), line.get() as u32);
|
||||
}
|
||||
if row.end_sequence() {
|
||||
// The next row is the start of a new sequence, which means we must
|
||||
@@ -363,16 +493,239 @@ fn line_info(obj_file: &File<'_>, sections: &mut [ObjSection]) -> Result<()> {
|
||||
}
|
||||
}
|
||||
|
||||
// COFF
|
||||
if let File::Coff(coff) = obj_file {
|
||||
line_info_coff(coff, sections, obj_data)?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn read(obj_path: &Path) -> Result<ObjInfo> {
|
||||
fn line_info_coff(coff: &CoffFile, sections: &mut [ObjSection], obj_data: &[u8]) -> Result<()> {
|
||||
let symbol_table = coff.coff_header().symbols(obj_data)?;
|
||||
|
||||
// Enumerate over all sections.
|
||||
for sect in coff.sections() {
|
||||
let ptr_linenums = sect.coff_section().pointer_to_linenumbers.get(LE) as usize;
|
||||
let num_linenums = sect.coff_section().number_of_linenumbers.get(LE) as usize;
|
||||
|
||||
// If we have no line number, skip this section.
|
||||
if num_linenums == 0 {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Find this section in our out_section. If it's not in out_section,
|
||||
// skip it.
|
||||
let Some(out_section) = sections.iter_mut().find(|s| s.orig_index == sect.index().0) else {
|
||||
continue;
|
||||
};
|
||||
|
||||
// Turn the line numbers into an ImageLinenumber slice.
|
||||
let Some(linenums) =
|
||||
&obj_data.get(ptr_linenums..ptr_linenums + num_linenums * size_of::<ImageLinenumber>())
|
||||
else {
|
||||
continue;
|
||||
};
|
||||
let Ok(linenums) = object::pod::slice_from_all_bytes::<ImageLinenumber>(linenums) else {
|
||||
continue;
|
||||
};
|
||||
|
||||
// In COFF, the line numbers are stored relative to the start of the
|
||||
// function. Because of this, we need to know the line number where the
|
||||
// function starts, so we can sum the two and get the line number
|
||||
// relative to the start of the file.
|
||||
//
|
||||
// This variable stores the line number where the function currently
|
||||
// being processed starts. It is set to None when we failed to find the
|
||||
// line number of the start of the function.
|
||||
let mut cur_fun_start_linenumber = None;
|
||||
for linenum in linenums {
|
||||
let line_number = linenum.linenumber.get(LE);
|
||||
if line_number == 0 {
|
||||
// Starting a new function. We need to find the line where that
|
||||
// function is located in the file. To do this, we need to find
|
||||
// the `.bf` symbol "associated" with this function. The .bf
|
||||
// symbol will have a Function Begin/End Auxillary Record, which
|
||||
// contains the line number of the start of the function.
|
||||
|
||||
// First, set cur_fun_start_linenumber to None. If we fail to
|
||||
// find the start of the function, this will make sure the
|
||||
// subsequent line numbers will be ignored until the next start
|
||||
// of function.
|
||||
cur_fun_start_linenumber = None;
|
||||
|
||||
// Get the symbol associated with this function. We'll need it
|
||||
// for logging purposes, but also to acquire its Function
|
||||
// Auxillary Record, which tells us where to find our .bf symbol.
|
||||
let symtable_entry = linenum.symbol_table_index_or_virtual_address.get(LE);
|
||||
let Ok(symbol) = symbol_table.symbol(SymbolIndex(symtable_entry as usize)) else {
|
||||
continue;
|
||||
};
|
||||
let Ok(aux_fun) = symbol_table.aux_function(SymbolIndex(symtable_entry as usize))
|
||||
else {
|
||||
continue;
|
||||
};
|
||||
|
||||
// Get the .bf symbol associated with this symbol. To do so, we
|
||||
// look at the Function Auxillary Record's tag_index, which is
|
||||
// an index in the symbol table pointing to our .bf symbol.
|
||||
if aux_fun.tag_index.get(LE) == 0 {
|
||||
continue;
|
||||
}
|
||||
let Ok(bf_symbol) =
|
||||
symbol_table.symbol(SymbolIndex(aux_fun.tag_index.get(LE) as usize))
|
||||
else {
|
||||
continue;
|
||||
};
|
||||
// Do some sanity checks that we are, indeed, looking at a .bf
|
||||
// symbol.
|
||||
if bf_symbol.name(symbol_table.strings()) != Ok(b".bf") {
|
||||
continue;
|
||||
}
|
||||
// Get the Function Begin/End Auxillary Record associated with
|
||||
// our .bf symbol, where we'll fine the linenumber of the start
|
||||
// of our function.
|
||||
let Ok(bf_aux) = symbol_table.get::<ImageAuxSymbolFunctionBeginEnd>(
|
||||
SymbolIndex(aux_fun.tag_index.get(LE) as usize),
|
||||
1,
|
||||
) else {
|
||||
continue;
|
||||
};
|
||||
// Set cur_fun_start_linenumber so the following linenumber
|
||||
// records will know at what line the current function start.
|
||||
cur_fun_start_linenumber = Some(bf_aux.linenumber.get(LE) as u32);
|
||||
// Let's also synthesize a line number record from the start of
|
||||
// the function, as the linenumber records don't always cover it.
|
||||
out_section.line_info.insert(
|
||||
sect.address() + symbol.value() as u64,
|
||||
bf_aux.linenumber.get(LE) as u32,
|
||||
);
|
||||
} else if let Some(cur_linenumber) = cur_fun_start_linenumber {
|
||||
let vaddr = linenum.symbol_table_index_or_virtual_address.get(LE);
|
||||
out_section
|
||||
.line_info
|
||||
.insert(sect.address() + vaddr as u64, cur_linenumber + line_number as u32);
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn update_combined_symbol(symbol: ObjSymbol, address_change: i64) -> Result<ObjSymbol> {
|
||||
Ok(ObjSymbol {
|
||||
name: symbol.name,
|
||||
demangled_name: symbol.demangled_name,
|
||||
has_extab: symbol.has_extab,
|
||||
extab_name: symbol.extab_name,
|
||||
extabindex_name: symbol.extabindex_name,
|
||||
address: (symbol.address as i64 + address_change).try_into()?,
|
||||
section_address: (symbol.section_address as i64 + address_change).try_into()?,
|
||||
size: symbol.size,
|
||||
size_known: symbol.size_known,
|
||||
flags: symbol.flags,
|
||||
addend: symbol.addend,
|
||||
virtual_address: if let Some(virtual_address) = symbol.virtual_address {
|
||||
Some((virtual_address as i64 + address_change).try_into()?)
|
||||
} else {
|
||||
None
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
fn combine_sections(section: ObjSection, combine: ObjSection) -> Result<ObjSection> {
|
||||
let mut data = section.data;
|
||||
data.extend(combine.data);
|
||||
|
||||
let address_change: i64 = (section.address + section.size) as i64 - combine.address as i64;
|
||||
let mut symbols = section.symbols;
|
||||
for symbol in combine.symbols {
|
||||
symbols.push(update_combined_symbol(symbol, address_change)?);
|
||||
}
|
||||
|
||||
let mut relocations = section.relocations;
|
||||
for reloc in combine.relocations {
|
||||
relocations.push(ObjReloc {
|
||||
flags: reloc.flags,
|
||||
address: (reloc.address as i64 + address_change).try_into()?,
|
||||
target: reloc.target, // TODO: Should be updated?
|
||||
target_section: reloc.target_section, // TODO: Same as above
|
||||
});
|
||||
}
|
||||
|
||||
let mut line_info = section.line_info;
|
||||
for (addr, line) in combine.line_info {
|
||||
let key = (addr as i64 + address_change).try_into()?;
|
||||
line_info.insert(key, line);
|
||||
}
|
||||
|
||||
Ok(ObjSection {
|
||||
name: section.name,
|
||||
kind: section.kind,
|
||||
address: section.address,
|
||||
size: section.size + combine.size,
|
||||
data,
|
||||
orig_index: section.orig_index,
|
||||
symbols,
|
||||
relocations,
|
||||
virtual_address: section.virtual_address,
|
||||
line_info,
|
||||
})
|
||||
}
|
||||
|
||||
fn combine_data_sections(sections: &mut Vec<ObjSection>) -> Result<()> {
|
||||
let names_to_combine: HashSet<_> = sections
|
||||
.iter()
|
||||
.filter(|s| s.kind == ObjSectionKind::Data)
|
||||
.map(|s| s.name.clone())
|
||||
.collect();
|
||||
|
||||
for name in names_to_combine {
|
||||
// Take section with lowest index
|
||||
let (mut section_index, _) = sections
|
||||
.iter()
|
||||
.enumerate()
|
||||
.filter(|(_, s)| s.name == name)
|
||||
.min_by_key(|(_, s)| s.orig_index)
|
||||
// Should not happen
|
||||
.context("No combine section found with name")?;
|
||||
let mut section = sections.remove(section_index);
|
||||
|
||||
// Remove equally named sections
|
||||
let mut combines = vec![];
|
||||
for i in (0..sections.len()).rev() {
|
||||
if sections[i].name != name || sections[i].orig_index == section.orig_index {
|
||||
continue;
|
||||
}
|
||||
combines.push(sections.remove(i));
|
||||
if i < section_index {
|
||||
section_index -= 1;
|
||||
}
|
||||
}
|
||||
|
||||
// Combine sections ordered by index
|
||||
combines.sort_unstable_by_key(|c| c.orig_index);
|
||||
for combine in combines {
|
||||
section = combine_sections(section, combine)?;
|
||||
}
|
||||
sections.insert(section_index, section);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn read(obj_path: &Path, config: &DiffObjConfig) -> Result<ObjInfo> {
|
||||
let (data, timestamp) = {
|
||||
let file = fs::File::open(obj_path)?;
|
||||
let timestamp = FileTime::from_last_modification_time(&file.metadata()?);
|
||||
(unsafe { memmap2::Mmap::map(&file) }?, timestamp)
|
||||
};
|
||||
let obj_file = File::parse(&*data)?;
|
||||
let mut obj = parse(&data, config)?;
|
||||
obj.path = Some(obj_path.to_owned());
|
||||
obj.timestamp = Some(timestamp);
|
||||
Ok(obj)
|
||||
}
|
||||
|
||||
pub fn parse(data: &[u8], config: &DiffObjConfig) -> Result<ObjInfo> {
|
||||
let obj_file = File::parse(data)?;
|
||||
let arch = new_arch(&obj_file)?;
|
||||
let split_meta = split_meta(&obj_file)?;
|
||||
let mut sections = filter_sections(&obj_file, split_meta.as_ref())?;
|
||||
@@ -382,9 +735,13 @@ pub fn read(obj_path: &Path) -> Result<ObjInfo> {
|
||||
section.relocations =
|
||||
relocations_by_section(arch.as_ref(), &obj_file, section, split_meta.as_ref())?;
|
||||
}
|
||||
line_info(&obj_file, &mut sections)?;
|
||||
if config.combine_data_sections {
|
||||
combine_data_sections(&mut sections)?;
|
||||
}
|
||||
line_info(&obj_file, &mut sections, data)?;
|
||||
let common = common_symbols(arch.as_ref(), &obj_file, split_meta.as_ref())?;
|
||||
Ok(ObjInfo { arch, path: obj_path.to_owned(), timestamp, sections, common, split_meta })
|
||||
let extab = exception_tables(&mut sections, &obj_file)?;
|
||||
Ok(ObjInfo { arch, path: None, timestamp: None, sections, common, extab, split_meta })
|
||||
}
|
||||
|
||||
pub fn has_function(obj_path: &Path, symbol_name: &str) -> Result<bool> {
|
||||
|
||||
@@ -1,9 +1,15 @@
|
||||
use std::fmt::{LowerHex, UpperHex};
|
||||
use std::{
|
||||
fmt::{LowerHex, UpperHex},
|
||||
io::Read,
|
||||
};
|
||||
|
||||
use anyhow::Result;
|
||||
use byteorder::{NativeEndian, ReadBytesExt};
|
||||
use num_traits::PrimInt;
|
||||
use object::{Endian, Object};
|
||||
|
||||
// https://stackoverflow.com/questions/44711012/how-do-i-format-a-signed-integer-to-a-sign-aware-hexadecimal-representation
|
||||
pub(crate) struct ReallySigned<N: PrimInt>(pub(crate) N);
|
||||
pub struct ReallySigned<N: PrimInt>(pub(crate) N);
|
||||
|
||||
impl<N: PrimInt> LowerHex for ReallySigned<N> {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
|
||||
@@ -22,3 +28,11 @@ impl<N: PrimInt> UpperHex for ReallySigned<N> {
|
||||
f.pad_integral(num >= 0, prefix, &bare_hex)
|
||||
}
|
||||
}
|
||||
|
||||
pub fn read_u32<R: Read>(obj_file: &object::File, reader: &mut R) -> Result<u32> {
|
||||
Ok(obj_file.endianness().read_u32(reader.read_u32::<NativeEndian>()?))
|
||||
}
|
||||
|
||||
pub fn read_u16<R: Read>(obj_file: &object::File, reader: &mut R) -> Result<u16> {
|
||||
Ok(obj_file.endianness().read_u16(reader.read_u16::<NativeEndian>()?))
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "objdiff-gui"
|
||||
version = "2.0.0-alpha.1"
|
||||
version = "2.0.0-beta.6"
|
||||
edition = "2021"
|
||||
rust-version = "1.70"
|
||||
authors = ["Luke Street <luke@street.dev>"]
|
||||
@@ -18,8 +18,9 @@ name = "objdiff"
|
||||
path = "src/main.rs"
|
||||
|
||||
[features]
|
||||
default = ["wgpu", "wsl"]
|
||||
wgpu = ["eframe/wgpu"]
|
||||
default = ["glow", "wgpu", "wsl"]
|
||||
glow = ["eframe/glow"]
|
||||
wgpu = ["eframe/wgpu", "dep:wgpu"]
|
||||
wsl = []
|
||||
|
||||
[dependencies]
|
||||
@@ -28,8 +29,8 @@ bytes = "1.6.0"
|
||||
cfg-if = "1.0.0"
|
||||
const_format = "0.2.32"
|
||||
cwdemangle = "1.0.0"
|
||||
cwextab = "0.2.3"
|
||||
dirs = "5.0.1"
|
||||
eframe = { version = "0.27.2", features = ["persistence"] }
|
||||
egui = "0.27.2"
|
||||
egui_extras = "0.27.2"
|
||||
filetime = "0.2.23"
|
||||
@@ -37,18 +38,43 @@ float-ord = "0.3.2"
|
||||
font-kit = "0.13.0"
|
||||
globset = { version = "0.4.14", features = ["serde1"] }
|
||||
log = "0.4.21"
|
||||
notify = "6.1.1"
|
||||
notify = { git = "https://github.com/encounter/notify", rev = "4c1783e8e041b5f69d4cf1750b9f07e335a0771e" }
|
||||
objdiff-core = { path = "../objdiff-core", features = ["all"] }
|
||||
png = "0.17.13"
|
||||
pollster = "0.3.0"
|
||||
regex = "1.10.5"
|
||||
rfd = { version = "0.14.1" } #, default-features = false, features = ['xdg-portal']
|
||||
rlwinmdec = "1.0.1"
|
||||
ron = "0.8.1"
|
||||
serde = { version = "1", features = ["derive"] }
|
||||
serde_json = "1.0.116"
|
||||
shell-escape = "0.1.5"
|
||||
strum = { version = "0.26.2", features = ["derive"] }
|
||||
tempfile = "3.10.1"
|
||||
time = { version = "0.3.36", features = ["formatting", "local-offset"] }
|
||||
|
||||
# Keep version in sync with egui
|
||||
[dependencies.eframe]
|
||||
version = "0.27.2"
|
||||
features = [
|
||||
"default_fonts",
|
||||
"persistence",
|
||||
"wayland",
|
||||
"x11",
|
||||
]
|
||||
default-features = false
|
||||
|
||||
# Keep version in sync with eframe
|
||||
[dependencies.wgpu]
|
||||
version = "0.19.1"
|
||||
features = [
|
||||
"dx12",
|
||||
"metal",
|
||||
"webgpu",
|
||||
]
|
||||
optional = true
|
||||
default-features = false
|
||||
|
||||
# For Linux static binaries, use rustls
|
||||
[target.'cfg(target_os = "linux")'.dependencies]
|
||||
reqwest = { version = "0.12.4", default-features = false, features = ["blocking", "json", "multipart", "rustls-tls"] }
|
||||
|
||||
@@ -30,14 +30,17 @@ use crate::{
|
||||
views::{
|
||||
appearance::{appearance_window, Appearance},
|
||||
config::{
|
||||
config_ui, diff_config_window, project_window, ConfigViewState, CONFIG_DISABLED_TEXT,
|
||||
arch_config_window, config_ui, project_window, ConfigViewState, CONFIG_DISABLED_TEXT,
|
||||
},
|
||||
data_diff::data_diff_ui,
|
||||
debug::debug_window,
|
||||
demangle::{demangle_window, DemangleViewState},
|
||||
extab_diff::extab_diff_ui,
|
||||
frame_history::FrameHistory,
|
||||
function_diff::function_diff_ui,
|
||||
graphics::{graphics_window, GraphicsConfig, GraphicsViewState},
|
||||
jobs::jobs_ui,
|
||||
rlwinm::{rlwinm_decode_window, RlwinmDecodeViewState},
|
||||
symbol_diff::{symbol_diff_ui, DiffViewState, View},
|
||||
},
|
||||
};
|
||||
@@ -47,13 +50,17 @@ pub struct ViewState {
|
||||
pub jobs: JobQueue,
|
||||
pub config_state: ConfigViewState,
|
||||
pub demangle_state: DemangleViewState,
|
||||
pub rlwinm_decode_state: RlwinmDecodeViewState,
|
||||
pub diff_state: DiffViewState,
|
||||
pub graphics_state: GraphicsViewState,
|
||||
pub frame_history: FrameHistory,
|
||||
pub show_appearance_config: bool,
|
||||
pub show_demangle: bool,
|
||||
pub show_rlwinm_decode: bool,
|
||||
pub show_project_config: bool,
|
||||
pub show_diff_config: bool,
|
||||
pub show_arch_config: bool,
|
||||
pub show_debug: bool,
|
||||
pub show_graphics: bool,
|
||||
}
|
||||
|
||||
/// The configuration for a single object file.
|
||||
@@ -209,6 +216,7 @@ pub struct App {
|
||||
config: AppConfigRef,
|
||||
modified: Arc<AtomicBool>,
|
||||
watcher: Option<notify::RecommendedWatcher>,
|
||||
app_path: Option<PathBuf>,
|
||||
relaunch_path: Rc<Mutex<Option<PathBuf>>>,
|
||||
should_relaunch: bool,
|
||||
}
|
||||
@@ -222,6 +230,9 @@ impl App {
|
||||
cc: &eframe::CreationContext<'_>,
|
||||
utc_offset: UtcOffset,
|
||||
relaunch_path: Rc<Mutex<Option<PathBuf>>>,
|
||||
app_path: Option<PathBuf>,
|
||||
graphics_config: GraphicsConfig,
|
||||
graphics_config_path: Option<PathBuf>,
|
||||
) -> Self {
|
||||
// Load previous app state (if any).
|
||||
// Note that you must enable the `persistence` feature for this to work.
|
||||
@@ -244,7 +255,32 @@ impl App {
|
||||
}
|
||||
app.appearance.init_fonts(&cc.egui_ctx);
|
||||
app.appearance.utc_offset = utc_offset;
|
||||
app.app_path = app_path;
|
||||
app.relaunch_path = relaunch_path;
|
||||
#[cfg(feature = "wgpu")]
|
||||
if let Some(wgpu_render_state) = &cc.wgpu_render_state {
|
||||
use eframe::egui_wgpu::wgpu::Backend;
|
||||
let info = wgpu_render_state.adapter.get_info();
|
||||
app.view_state.graphics_state.active_backend = match info.backend {
|
||||
Backend::Empty => "Unknown",
|
||||
Backend::Vulkan => "Vulkan",
|
||||
Backend::Metal => "Metal",
|
||||
Backend::Dx12 => "DirectX 12",
|
||||
Backend::Gl => "OpenGL",
|
||||
Backend::BrowserWebGpu => "WebGPU",
|
||||
}
|
||||
.to_string();
|
||||
app.view_state.graphics_state.active_device.clone_from(&info.name);
|
||||
}
|
||||
#[cfg(feature = "glow")]
|
||||
if let Some(gl) = &cc.gl {
|
||||
use eframe::glow::HasContext;
|
||||
app.view_state.graphics_state.active_backend = "OpenGL (Fallback)".to_string();
|
||||
app.view_state.graphics_state.active_device =
|
||||
unsafe { gl.get_parameter_string(0x1F01) }; // GL_RENDERER
|
||||
}
|
||||
app.view_state.graphics_state.graphics_config = graphics_config;
|
||||
app.view_state.graphics_state.graphics_config_path = graphics_config_path;
|
||||
app
|
||||
}
|
||||
|
||||
@@ -267,8 +303,8 @@ impl App {
|
||||
JobResult::Update(state) => {
|
||||
if let Ok(mut guard) = self.relaunch_path.lock() {
|
||||
*guard = Some(state.exe_path);
|
||||
self.should_relaunch = true;
|
||||
}
|
||||
self.should_relaunch = true;
|
||||
}
|
||||
_ => results.push(result),
|
||||
}
|
||||
@@ -308,7 +344,7 @@ impl App {
|
||||
fn post_update(&mut self, ctx: &egui::Context) {
|
||||
self.appearance.post_update(ctx);
|
||||
|
||||
let ViewState { jobs, diff_state, config_state, .. } = &mut self.view_state;
|
||||
let ViewState { jobs, diff_state, config_state, graphics_state, .. } = &mut self.view_state;
|
||||
config_state.post_update(ctx, jobs, &self.config);
|
||||
diff_state.post_update(ctx, jobs, &self.config);
|
||||
|
||||
@@ -365,13 +401,17 @@ impl App {
|
||||
|
||||
if let Some(result) = &diff_state.build {
|
||||
if let Some((obj, _)) = &result.first_obj {
|
||||
if file_modified(&obj.path, obj.timestamp) {
|
||||
config.queue_reload = true;
|
||||
if let (Some(path), Some(timestamp)) = (&obj.path, obj.timestamp) {
|
||||
if file_modified(path, timestamp) {
|
||||
config.queue_reload = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
if let Some((obj, _)) = &result.second_obj {
|
||||
if file_modified(&obj.path, obj.timestamp) {
|
||||
config.queue_reload = true;
|
||||
if let (Some(path), Some(timestamp)) = (&obj.path, obj.timestamp) {
|
||||
if file_modified(path, timestamp) {
|
||||
config.queue_reload = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -390,6 +430,15 @@ impl App {
|
||||
jobs.push(start_build(ctx, diff_config));
|
||||
config.queue_reload = false;
|
||||
}
|
||||
|
||||
if graphics_state.should_relaunch {
|
||||
if let Some(app_path) = &self.app_path {
|
||||
if let Ok(mut guard) = self.relaunch_path.lock() {
|
||||
*guard = Some(app_path.clone());
|
||||
self.should_relaunch = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -409,13 +458,17 @@ impl eframe::App for App {
|
||||
jobs,
|
||||
config_state,
|
||||
demangle_state,
|
||||
rlwinm_decode_state,
|
||||
diff_state,
|
||||
graphics_state,
|
||||
frame_history,
|
||||
show_appearance_config,
|
||||
show_demangle,
|
||||
show_rlwinm_decode,
|
||||
show_project_config,
|
||||
show_diff_config,
|
||||
show_arch_config,
|
||||
show_debug,
|
||||
show_graphics,
|
||||
} = view_state;
|
||||
|
||||
frame_history.on_new_frame(ctx.input(|i| i.time), frame.info().cpu_usage);
|
||||
@@ -457,6 +510,10 @@ impl eframe::App for App {
|
||||
*show_appearance_config = !*show_appearance_config;
|
||||
ui.close_menu();
|
||||
}
|
||||
if ui.button("Graphics…").clicked() {
|
||||
*show_graphics = !*show_graphics;
|
||||
ui.close_menu();
|
||||
}
|
||||
if ui.button("Quit").clicked() {
|
||||
ctx.send_viewport_cmd(egui::ViewportCommand::Close);
|
||||
}
|
||||
@@ -466,10 +523,14 @@ impl eframe::App for App {
|
||||
*show_demangle = !*show_demangle;
|
||||
ui.close_menu();
|
||||
}
|
||||
if ui.button("Rlwinm Decoder…").clicked() {
|
||||
*show_rlwinm_decode = !*show_rlwinm_decode;
|
||||
ui.close_menu();
|
||||
}
|
||||
});
|
||||
ui.menu_button("Diff Options", |ui| {
|
||||
if ui.button("More…").clicked() {
|
||||
*show_diff_config = !*show_diff_config;
|
||||
if ui.button("Arch Settings…").clicked() {
|
||||
*show_arch_config = !*show_arch_config;
|
||||
ui.close_menu();
|
||||
}
|
||||
let mut config = config.write().unwrap();
|
||||
@@ -512,6 +573,16 @@ impl eframe::App for App {
|
||||
{
|
||||
config.queue_reload = true;
|
||||
}
|
||||
if ui
|
||||
.checkbox(
|
||||
&mut config.diff_obj_config.combine_data_sections,
|
||||
"Combine data sections",
|
||||
)
|
||||
.on_hover_text("Combines data sections with equal names.")
|
||||
.changed()
|
||||
{
|
||||
config.queue_reload = true;
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -525,6 +596,10 @@ impl eframe::App for App {
|
||||
egui::CentralPanel::default().show(ctx, |ui| {
|
||||
data_diff_ui(ui, diff_state, appearance);
|
||||
});
|
||||
} else if diff_state.current_view == View::ExtabDiff && build_success {
|
||||
egui::CentralPanel::default().show(ctx, |ui| {
|
||||
extab_diff_ui(ui, diff_state, appearance);
|
||||
});
|
||||
} else {
|
||||
egui::SidePanel::left("side_panel").show(ctx, |ui| {
|
||||
egui::ScrollArea::both().show(ui, |ui| {
|
||||
@@ -541,8 +616,10 @@ impl eframe::App for App {
|
||||
project_window(ctx, config, show_project_config, config_state, appearance);
|
||||
appearance_window(ctx, show_appearance_config, appearance);
|
||||
demangle_window(ctx, show_demangle, demangle_state, appearance);
|
||||
diff_config_window(ctx, config, show_diff_config, appearance);
|
||||
rlwinm_decode_window(ctx, show_rlwinm_decode, rlwinm_decode_state, appearance);
|
||||
arch_config_window(ctx, config, show_arch_config, appearance);
|
||||
debug_window(ctx, show_debug, frame_history, appearance);
|
||||
graphics_window(ctx, show_graphics, frame_history, graphics_state, appearance);
|
||||
|
||||
self.post_update(ctx);
|
||||
}
|
||||
|
||||
@@ -40,6 +40,7 @@ pub struct BuildConfig {
|
||||
pub project_dir: Option<PathBuf>,
|
||||
pub custom_make: Option<String>,
|
||||
pub custom_args: Option<Vec<String>>,
|
||||
#[allow(unused)]
|
||||
pub selected_wsl_distro: Option<String>,
|
||||
}
|
||||
|
||||
@@ -143,7 +144,7 @@ fn run_make_cmd(config: &BuildConfig, cwd: &Path, arg: &Path) -> Result<BuildSta
|
||||
cmdline.push(' ');
|
||||
cmdline.push_str(shell_escape::escape(arg.to_string_lossy()).as_ref());
|
||||
}
|
||||
let output = command.output().context("Failed to execute build")?;
|
||||
let output = command.output().map_err(|e| anyhow!("Failed to execute build: {e}"))?;
|
||||
let stdout = from_utf8(&output.stdout).context("Failed to process stdout")?;
|
||||
let stderr = from_utf8(&output.stderr).context("Failed to process stderr")?;
|
||||
Ok(BuildStatus {
|
||||
@@ -235,7 +236,7 @@ fn run_build(
|
||||
total,
|
||||
&cancel,
|
||||
)?;
|
||||
Some(read::read(target_path).with_context(|| {
|
||||
Some(read::read(target_path, &config.diff_obj_config).with_context(|| {
|
||||
format!("Failed to read object '{}'", target_path.display())
|
||||
})?)
|
||||
}
|
||||
@@ -252,7 +253,7 @@ fn run_build(
|
||||
&cancel,
|
||||
)?;
|
||||
Some(
|
||||
read::read(base_path)
|
||||
read::read(base_path, &config.diff_obj_config)
|
||||
.with_context(|| format!("Failed to read object '{}'", base_path.display()))?,
|
||||
)
|
||||
}
|
||||
|
||||
@@ -11,6 +11,7 @@ mod views;
|
||||
|
||||
use std::{
|
||||
path::PathBuf,
|
||||
process::ExitCode,
|
||||
rc::Rc,
|
||||
sync::{Arc, Mutex},
|
||||
};
|
||||
@@ -19,6 +20,8 @@ use anyhow::{ensure, Result};
|
||||
use cfg_if::cfg_if;
|
||||
use time::UtcOffset;
|
||||
|
||||
use crate::views::graphics::{load_graphics_config, GraphicsBackend, GraphicsConfig};
|
||||
|
||||
fn load_icon() -> Result<egui::IconData> {
|
||||
use bytes::Buf;
|
||||
let decoder = png::Decoder::new(include_bytes!("../assets/icon_64.png").reader());
|
||||
@@ -31,9 +34,11 @@ fn load_icon() -> Result<egui::IconData> {
|
||||
Ok(egui::IconData { rgba: buf, width: info.width, height: info.height })
|
||||
}
|
||||
|
||||
const APP_NAME: &str = "objdiff";
|
||||
|
||||
// When compiling natively:
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
fn main() {
|
||||
fn main() -> ExitCode {
|
||||
// Log to stdout (if you run with `RUST_LOG=debug`).
|
||||
tracing_subscriber::fmt::init();
|
||||
|
||||
@@ -42,8 +47,8 @@ fn main() {
|
||||
// https://github.com/time-rs/time/issues/293
|
||||
let utc_offset = UtcOffset::current_local_offset().unwrap_or(UtcOffset::UTC);
|
||||
|
||||
let app_path = std::env::current_exe().ok();
|
||||
let exec_path: Rc<Mutex<Option<PathBuf>>> = Rc::new(Mutex::new(None));
|
||||
let exec_path_clone = exec_path.clone();
|
||||
let mut native_options =
|
||||
eframe::NativeOptions { follow_system_theme: false, ..Default::default() };
|
||||
match load_icon() {
|
||||
@@ -51,42 +56,149 @@ fn main() {
|
||||
native_options.viewport.icon = Some(Arc::new(data));
|
||||
}
|
||||
Err(e) => {
|
||||
log::warn!("Failed to load application icon: {}", e);
|
||||
log::warn!("Failed to load application icon: {e:?}");
|
||||
}
|
||||
}
|
||||
let mut graphics_config = GraphicsConfig::default();
|
||||
let mut graphics_config_path = None;
|
||||
if let Some(storage_dir) = eframe::storage_dir(APP_NAME) {
|
||||
let config_path = storage_dir.join("graphics.ron");
|
||||
match load_graphics_config(&config_path) {
|
||||
Ok(Some(config)) => {
|
||||
graphics_config = config;
|
||||
}
|
||||
Ok(None) => {}
|
||||
Err(e) => {
|
||||
log::error!("Failed to load native config: {e:?}");
|
||||
}
|
||||
}
|
||||
graphics_config_path = Some(config_path);
|
||||
}
|
||||
#[cfg(feature = "wgpu")]
|
||||
{
|
||||
native_options.renderer = eframe::Renderer::Wgpu;
|
||||
use eframe::egui_wgpu::wgpu::Backends;
|
||||
if graphics_config.desired_backend.is_supported() {
|
||||
native_options.wgpu_options.supported_backends = match graphics_config.desired_backend {
|
||||
GraphicsBackend::Auto => native_options.wgpu_options.supported_backends,
|
||||
GraphicsBackend::Dx12 => Backends::DX12,
|
||||
GraphicsBackend::Metal => Backends::METAL,
|
||||
GraphicsBackend::Vulkan => Backends::VULKAN,
|
||||
GraphicsBackend::OpenGL => Backends::GL,
|
||||
};
|
||||
}
|
||||
}
|
||||
let mut eframe_error = None;
|
||||
if let Err(e) = run_eframe(
|
||||
native_options.clone(),
|
||||
utc_offset,
|
||||
exec_path.clone(),
|
||||
app_path.clone(),
|
||||
graphics_config.clone(),
|
||||
graphics_config_path.clone(),
|
||||
) {
|
||||
eframe_error = Some(e);
|
||||
}
|
||||
#[cfg(feature = "wgpu")]
|
||||
if let Some(e) = eframe_error {
|
||||
// Attempt to relaunch using wgpu auto backend if the desired backend failed
|
||||
#[allow(unused_mut)]
|
||||
let mut should_relaunch = graphics_config.desired_backend != GraphicsBackend::Auto;
|
||||
#[cfg(feature = "glow")]
|
||||
{
|
||||
// If the desired backend is OpenGL, we should try to relaunch using the glow renderer
|
||||
should_relaunch &= graphics_config.desired_backend != GraphicsBackend::OpenGL;
|
||||
}
|
||||
if should_relaunch {
|
||||
log::warn!("Failed to launch application: {e:?}");
|
||||
log::warn!("Attempting to relaunch using auto-detected backend");
|
||||
native_options.wgpu_options.supported_backends = Default::default();
|
||||
if let Err(e) = run_eframe(
|
||||
native_options.clone(),
|
||||
utc_offset,
|
||||
exec_path.clone(),
|
||||
app_path.clone(),
|
||||
graphics_config.clone(),
|
||||
graphics_config_path.clone(),
|
||||
) {
|
||||
eframe_error = Some(e);
|
||||
} else {
|
||||
eframe_error = None;
|
||||
}
|
||||
} else {
|
||||
eframe_error = Some(e);
|
||||
}
|
||||
}
|
||||
#[cfg(all(feature = "wgpu", feature = "glow"))]
|
||||
if let Some(e) = eframe_error {
|
||||
// Attempt to relaunch using the glow renderer if the wgpu backend failed
|
||||
log::warn!("Failed to launch application: {e:?}");
|
||||
log::warn!("Attempting to relaunch using fallback OpenGL backend");
|
||||
native_options.renderer = eframe::Renderer::Glow;
|
||||
if let Err(e) = run_eframe(
|
||||
native_options,
|
||||
utc_offset,
|
||||
exec_path.clone(),
|
||||
app_path,
|
||||
graphics_config,
|
||||
graphics_config_path,
|
||||
) {
|
||||
eframe_error = Some(e);
|
||||
} else {
|
||||
eframe_error = None;
|
||||
}
|
||||
}
|
||||
if let Some(e) = eframe_error {
|
||||
log::error!("Failed to launch application: {e:?}");
|
||||
return ExitCode::FAILURE;
|
||||
}
|
||||
eframe::run_native(
|
||||
"objdiff",
|
||||
native_options,
|
||||
Box::new(move |cc| Box::new(app::App::new(cc, utc_offset, exec_path_clone))),
|
||||
)
|
||||
.expect("Failed to run eframe application");
|
||||
|
||||
// Attempt to relaunch application from the updated path
|
||||
if let Ok(mut guard) = exec_path.lock() {
|
||||
if let Some(path) = guard.take() {
|
||||
cfg_if! {
|
||||
if #[cfg(unix)] {
|
||||
let result = exec::Command::new(path)
|
||||
let e = exec::Command::new(path)
|
||||
.args(&std::env::args().collect::<Vec<String>>())
|
||||
.exec();
|
||||
log::error!("Failed to relaunch: {result:?}");
|
||||
log::error!("Failed to relaunch: {e:?}");
|
||||
return ExitCode::FAILURE;
|
||||
} else {
|
||||
let result = std::process::Command::new(path)
|
||||
.args(std::env::args())
|
||||
.spawn()
|
||||
.unwrap()
|
||||
.wait();
|
||||
.spawn();
|
||||
if let Err(e) = result {
|
||||
log::error!("Failed to relaunch: {:?}", e);
|
||||
log::error!("Failed to relaunch: {e:?}");
|
||||
return ExitCode::FAILURE;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
ExitCode::SUCCESS
|
||||
}
|
||||
|
||||
fn run_eframe(
|
||||
native_options: eframe::NativeOptions,
|
||||
utc_offset: UtcOffset,
|
||||
exec_path_clone: Rc<Mutex<Option<PathBuf>>>,
|
||||
app_path: Option<PathBuf>,
|
||||
graphics_config: GraphicsConfig,
|
||||
graphics_config_path: Option<PathBuf>,
|
||||
) -> Result<(), eframe::Error> {
|
||||
eframe::run_native(
|
||||
APP_NAME,
|
||||
native_options,
|
||||
Box::new(move |cc| {
|
||||
Box::new(app::App::new(
|
||||
cc,
|
||||
utc_offset,
|
||||
exec_path_clone,
|
||||
app_path,
|
||||
graphics_config,
|
||||
graphics_config_path,
|
||||
))
|
||||
}),
|
||||
)
|
||||
}
|
||||
|
||||
// when compiling to web using trunk.
|
||||
|
||||
@@ -119,6 +119,8 @@ impl Appearance {
|
||||
self.delete_color = Color32::from_rgb(200, 40, 41);
|
||||
}
|
||||
}
|
||||
style.spacing.scroll = egui::style::ScrollStyle::solid();
|
||||
style.spacing.scroll.bar_width = 10.0;
|
||||
ctx.set_style(style);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
#[cfg(all(windows, feature = "wsl"))]
|
||||
use std::string::FromUtf16Error;
|
||||
use std::{
|
||||
borrow::Cow,
|
||||
mem::take,
|
||||
path::{PathBuf, MAIN_SEPARATOR},
|
||||
};
|
||||
@@ -16,9 +15,10 @@ use egui::{
|
||||
use globset::Glob;
|
||||
use objdiff_core::{
|
||||
config::{ProjectObject, DEFAULT_WATCH_PATTERNS},
|
||||
diff::X86Formatter,
|
||||
diff::{ArmArchVersion, ArmR9Usage, MipsAbi, MipsInstrCategory, X86Formatter},
|
||||
};
|
||||
use self_update::cargo_crate_version;
|
||||
use strum::{EnumMessage, VariantArray};
|
||||
|
||||
use crate::{
|
||||
app::{AppConfig, AppConfigRef, ObjectConfig},
|
||||
@@ -49,6 +49,7 @@ pub struct ConfigViewState {
|
||||
pub object_search: String,
|
||||
pub filter_diffable: bool,
|
||||
pub filter_incomplete: bool,
|
||||
pub show_hidden: bool,
|
||||
#[cfg(all(windows, feature = "wsl"))]
|
||||
pub available_wsl_distros: Option<Vec<String>>,
|
||||
pub file_dialog_state: FileDialogState,
|
||||
@@ -241,7 +242,7 @@ pub fn config_ui(
|
||||
|| {
|
||||
Box::pin(
|
||||
rfd::AsyncFileDialog::new()
|
||||
.set_directory(&target_dir)
|
||||
.set_directory(target_dir)
|
||||
.add_filter("Object file", &["o", "elf", "obj"])
|
||||
.pick_file(),
|
||||
)
|
||||
@@ -282,20 +283,18 @@ pub fn config_ui(
|
||||
root_open = Some(true);
|
||||
node_open = NodeOpen::Object;
|
||||
}
|
||||
if ui
|
||||
.selectable_label(state.filter_diffable, "Diffable")
|
||||
.on_hover_text_at_pointer("Only show objects with a source file")
|
||||
.clicked()
|
||||
{
|
||||
state.filter_diffable = !state.filter_diffable;
|
||||
}
|
||||
if ui
|
||||
.selectable_label(state.filter_incomplete, "Incomplete")
|
||||
.on_hover_text_at_pointer("Only show objects not marked complete")
|
||||
.clicked()
|
||||
{
|
||||
state.filter_incomplete = !state.filter_incomplete;
|
||||
let mut filters_text = RichText::new("Filter ⏷");
|
||||
if state.filter_diffable || state.filter_incomplete || state.show_hidden {
|
||||
filters_text = filters_text.color(appearance.replace_color);
|
||||
}
|
||||
egui::menu::menu_button(ui, filters_text, |ui| {
|
||||
ui.checkbox(&mut state.filter_diffable, "Diffable")
|
||||
.on_hover_text_at_pointer("Only show objects with a source file");
|
||||
ui.checkbox(&mut state.filter_incomplete, "Incomplete")
|
||||
.on_hover_text_at_pointer("Only show objects not marked complete");
|
||||
ui.checkbox(&mut state.show_hidden, "Hidden")
|
||||
.on_hover_text_at_pointer("Show hidden (auto-generated) objects");
|
||||
});
|
||||
});
|
||||
if state.object_search.is_empty() {
|
||||
if had_search {
|
||||
@@ -314,27 +313,18 @@ pub fn config_ui(
|
||||
.open(root_open)
|
||||
.default_open(true)
|
||||
.show(ui, |ui| {
|
||||
let mut nodes = Cow::Borrowed(object_nodes);
|
||||
if !state.object_search.is_empty() || state.filter_diffable || state.filter_incomplete {
|
||||
let search = state.object_search.to_ascii_lowercase();
|
||||
nodes = Cow::Owned(
|
||||
object_nodes
|
||||
.iter()
|
||||
.filter_map(|node| {
|
||||
filter_node(
|
||||
node,
|
||||
&search,
|
||||
state.filter_diffable,
|
||||
state.filter_incomplete,
|
||||
)
|
||||
})
|
||||
.collect(),
|
||||
);
|
||||
}
|
||||
|
||||
let search = state.object_search.to_ascii_lowercase();
|
||||
ui.style_mut().wrap = Some(false);
|
||||
for node in nodes.iter() {
|
||||
display_node(ui, &mut new_selected_obj, node, appearance, node_open);
|
||||
for node in object_nodes.iter().filter_map(|node| {
|
||||
filter_node(
|
||||
node,
|
||||
&search,
|
||||
state.filter_diffable,
|
||||
state.filter_incomplete,
|
||||
state.show_hidden,
|
||||
)
|
||||
}) {
|
||||
display_node(ui, &mut new_selected_obj, &node, appearance, node_open);
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -364,7 +354,7 @@ fn display_object(
|
||||
let selected = matches!(selected_obj, Some(obj) if obj.name == object_name);
|
||||
let color = if selected {
|
||||
appearance.emphasized_text_color
|
||||
} else if let Some(complete) = object.complete {
|
||||
} else if let Some(complete) = object.complete() {
|
||||
if complete {
|
||||
appearance.insert_color
|
||||
} else {
|
||||
@@ -391,8 +381,8 @@ fn display_object(
|
||||
name: object_name.to_string(),
|
||||
target_path: object.target_path.clone(),
|
||||
base_path: object.base_path.clone(),
|
||||
reverse_fn_order: object.reverse_fn_order,
|
||||
complete: object.complete,
|
||||
reverse_fn_order: object.reverse_fn_order(),
|
||||
complete: object.complete(),
|
||||
scratch: object.scratch.clone(),
|
||||
});
|
||||
}
|
||||
@@ -463,13 +453,15 @@ fn filter_node(
|
||||
search: &str,
|
||||
filter_diffable: bool,
|
||||
filter_incomplete: bool,
|
||||
show_hidden: bool,
|
||||
) -> Option<ProjectObjectNode> {
|
||||
match node {
|
||||
ProjectObjectNode::File(name, object) => {
|
||||
if (search.is_empty() || name.to_ascii_lowercase().contains(search))
|
||||
&& (!filter_diffable
|
||||
|| (object.base_path.is_some() && object.target_path.is_some()))
|
||||
&& (!filter_incomplete || matches!(object.complete, None | Some(false)))
|
||||
&& (!filter_incomplete || matches!(object.complete(), None | Some(false)))
|
||||
&& (show_hidden || !object.hidden())
|
||||
{
|
||||
Some(node.clone())
|
||||
} else {
|
||||
@@ -477,15 +469,11 @@ fn filter_node(
|
||||
}
|
||||
}
|
||||
ProjectObjectNode::Dir(name, children) => {
|
||||
if (search.is_empty() || name.to_ascii_lowercase().contains(search))
|
||||
&& !filter_diffable
|
||||
&& !filter_incomplete
|
||||
{
|
||||
return Some(node.clone());
|
||||
}
|
||||
let new_children = children
|
||||
.iter()
|
||||
.filter_map(|child| filter_node(child, search, filter_diffable, filter_incomplete))
|
||||
.filter_map(|child| {
|
||||
filter_node(child, search, filter_diffable, filter_incomplete, show_hidden)
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
if !new_children.is_empty() {
|
||||
Some(ProjectObjectNode::Dir(name.clone(), new_children))
|
||||
@@ -842,29 +830,28 @@ fn split_obj_config_ui(
|
||||
});
|
||||
}
|
||||
|
||||
pub fn diff_config_window(
|
||||
pub fn arch_config_window(
|
||||
ctx: &egui::Context,
|
||||
config: &AppConfigRef,
|
||||
show: &mut bool,
|
||||
appearance: &Appearance,
|
||||
) {
|
||||
let mut config_guard = config.write().unwrap();
|
||||
egui::Window::new("Diff Config").open(show).show(ctx, |ui| {
|
||||
diff_config_ui(ui, &mut config_guard, appearance);
|
||||
egui::Window::new("Arch Settings").open(show).show(ctx, |ui| {
|
||||
arch_config_ui(ui, &mut config_guard, appearance);
|
||||
});
|
||||
}
|
||||
|
||||
fn diff_config_ui(ui: &mut egui::Ui, config: &mut AppConfig, _appearance: &Appearance) {
|
||||
egui::ComboBox::new("x86_formatter", "X86 Format")
|
||||
.selected_text(format!("{:?}", config.diff_obj_config.x86_formatter))
|
||||
fn arch_config_ui(ui: &mut egui::Ui, config: &mut AppConfig, _appearance: &Appearance) {
|
||||
ui.heading("x86");
|
||||
egui::ComboBox::new("x86_formatter", "Format")
|
||||
.selected_text(config.diff_obj_config.x86_formatter.get_message().unwrap())
|
||||
.show_ui(ui, |ui| {
|
||||
for &formatter in
|
||||
&[X86Formatter::Intel, X86Formatter::Gas, X86Formatter::Nasm, X86Formatter::Masm]
|
||||
{
|
||||
for &formatter in X86Formatter::VARIANTS {
|
||||
if ui
|
||||
.selectable_label(
|
||||
config.diff_obj_config.x86_formatter == formatter,
|
||||
format!("{:?}", formatter),
|
||||
formatter.get_message().unwrap(),
|
||||
)
|
||||
.clicked()
|
||||
{
|
||||
@@ -873,4 +860,103 @@ fn diff_config_ui(ui: &mut egui::Ui, config: &mut AppConfig, _appearance: &Appea
|
||||
}
|
||||
}
|
||||
});
|
||||
ui.separator();
|
||||
ui.heading("MIPS");
|
||||
egui::ComboBox::new("mips_abi", "ABI")
|
||||
.selected_text(config.diff_obj_config.mips_abi.get_message().unwrap())
|
||||
.show_ui(ui, |ui| {
|
||||
for &abi in MipsAbi::VARIANTS {
|
||||
if ui
|
||||
.selectable_label(
|
||||
config.diff_obj_config.mips_abi == abi,
|
||||
abi.get_message().unwrap(),
|
||||
)
|
||||
.clicked()
|
||||
{
|
||||
config.diff_obj_config.mips_abi = abi;
|
||||
config.queue_reload = true;
|
||||
}
|
||||
}
|
||||
});
|
||||
egui::ComboBox::new("mips_instr_category", "Instruction Category")
|
||||
.selected_text(config.diff_obj_config.mips_instr_category.get_message().unwrap())
|
||||
.show_ui(ui, |ui| {
|
||||
for &category in MipsInstrCategory::VARIANTS {
|
||||
if ui
|
||||
.selectable_label(
|
||||
config.diff_obj_config.mips_instr_category == category,
|
||||
category.get_message().unwrap(),
|
||||
)
|
||||
.clicked()
|
||||
{
|
||||
config.diff_obj_config.mips_instr_category = category;
|
||||
config.queue_reload = true;
|
||||
}
|
||||
}
|
||||
});
|
||||
ui.separator();
|
||||
ui.heading("ARM");
|
||||
egui::ComboBox::new("arm_arch_version", "Architecture Version")
|
||||
.selected_text(config.diff_obj_config.arm_arch_version.get_message().unwrap())
|
||||
.show_ui(ui, |ui| {
|
||||
for &version in ArmArchVersion::VARIANTS {
|
||||
if ui
|
||||
.selectable_label(
|
||||
config.diff_obj_config.arm_arch_version == version,
|
||||
version.get_message().unwrap(),
|
||||
)
|
||||
.clicked()
|
||||
{
|
||||
config.diff_obj_config.arm_arch_version = version;
|
||||
config.queue_reload = true;
|
||||
}
|
||||
}
|
||||
});
|
||||
let response = ui
|
||||
.checkbox(&mut config.diff_obj_config.arm_unified_syntax, "Unified syntax")
|
||||
.on_hover_text("Disassemble as unified assembly language (UAL).");
|
||||
if response.changed() {
|
||||
config.queue_reload = true;
|
||||
}
|
||||
let response = ui
|
||||
.checkbox(&mut config.diff_obj_config.arm_av_registers, "Use A/V registers")
|
||||
.on_hover_text("Display R0-R3 as A1-A4 and R4-R11 as V1-V8");
|
||||
if response.changed() {
|
||||
config.queue_reload = true;
|
||||
}
|
||||
egui::ComboBox::new("arm_r9_usage", "Display R9 as")
|
||||
.selected_text(config.diff_obj_config.arm_r9_usage.get_message().unwrap())
|
||||
.show_ui(ui, |ui| {
|
||||
for &usage in ArmR9Usage::VARIANTS {
|
||||
if ui
|
||||
.selectable_label(
|
||||
config.diff_obj_config.arm_r9_usage == usage,
|
||||
usage.get_message().unwrap(),
|
||||
)
|
||||
.on_hover_text(usage.get_detailed_message().unwrap())
|
||||
.clicked()
|
||||
{
|
||||
config.diff_obj_config.arm_r9_usage = usage;
|
||||
config.queue_reload = true;
|
||||
}
|
||||
}
|
||||
});
|
||||
let response = ui
|
||||
.checkbox(&mut config.diff_obj_config.arm_sl_usage, "Display R10 as SL")
|
||||
.on_hover_text("Used for explicit stack limits.");
|
||||
if response.changed() {
|
||||
config.queue_reload = true;
|
||||
}
|
||||
let response = ui
|
||||
.checkbox(&mut config.diff_obj_config.arm_fp_usage, "Display R11 as FP")
|
||||
.on_hover_text("Used for frame pointers.");
|
||||
if response.changed() {
|
||||
config.queue_reload = true;
|
||||
}
|
||||
let response = ui
|
||||
.checkbox(&mut config.diff_obj_config.arm_ip_usage, "Display R12 as IP")
|
||||
.on_hover_text("Used for interworking and long branches.");
|
||||
if response.changed() {
|
||||
config.queue_reload = true;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,6 +12,9 @@ pub fn debug_window(
|
||||
}
|
||||
|
||||
fn debug_ui(ui: &mut egui::Ui, frame_history: &mut FrameHistory, _appearance: &Appearance) {
|
||||
if ui.button("Clear memory").clicked() {
|
||||
ui.memory_mut(|m| *m = Default::default());
|
||||
}
|
||||
ui.label(format!("Repainting the UI each frame. FPS: {:.1}", frame_history.fps()));
|
||||
frame_history.ui(ui);
|
||||
}
|
||||
|
||||
218
objdiff-gui/src/views/extab_diff.rs
Normal file
218
objdiff-gui/src/views/extab_diff.rs
Normal file
@@ -0,0 +1,218 @@
|
||||
use egui::{text::LayoutJob, Align, Layout, ScrollArea, Ui, Vec2};
|
||||
use egui_extras::{Size, StripBuilder};
|
||||
use objdiff_core::{
|
||||
diff::ObjDiff,
|
||||
obj::{ObjExtab, ObjInfo, ObjSymbol, SymbolRef},
|
||||
};
|
||||
use time::format_description;
|
||||
|
||||
use crate::views::{
|
||||
appearance::Appearance,
|
||||
symbol_diff::{match_color_for_symbol, DiffViewState, SymbolRefByName, View},
|
||||
};
|
||||
|
||||
fn find_symbol(obj: &ObjInfo, selected_symbol: &SymbolRefByName) -> Option<SymbolRef> {
|
||||
for (section_idx, section) in obj.sections.iter().enumerate() {
|
||||
for (symbol_idx, symbol) in section.symbols.iter().enumerate() {
|
||||
if symbol.name == selected_symbol.symbol_name {
|
||||
return Some(SymbolRef { section_idx, symbol_idx });
|
||||
}
|
||||
}
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
fn decode_extab(extab: &ObjExtab) -> String {
|
||||
let mut text = String::from("");
|
||||
|
||||
let mut dtor_names: Vec<&str> = vec![];
|
||||
for dtor in &extab.dtors {
|
||||
//For each function name, use the demangled name by default,
|
||||
//and if not available fallback to the original name
|
||||
let name = match &dtor.demangled_name {
|
||||
Some(demangled_name) => demangled_name,
|
||||
None => &dtor.name,
|
||||
};
|
||||
dtor_names.push(name.as_str());
|
||||
}
|
||||
if let Some(decoded) = extab.data.to_string(&dtor_names) {
|
||||
text += decoded.as_str();
|
||||
}
|
||||
|
||||
text
|
||||
}
|
||||
|
||||
fn find_extab_entry(obj: &ObjInfo, symbol: &ObjSymbol) -> Option<ObjExtab> {
|
||||
if let Some(extab_array) = &obj.extab {
|
||||
for extab_entry in extab_array {
|
||||
if extab_entry.func.name == symbol.name {
|
||||
return Some(extab_entry.clone());
|
||||
}
|
||||
}
|
||||
} else {
|
||||
return None;
|
||||
}
|
||||
|
||||
None
|
||||
}
|
||||
|
||||
fn extab_text_ui(
|
||||
ui: &mut Ui,
|
||||
obj: &(ObjInfo, ObjDiff),
|
||||
symbol_ref: SymbolRef,
|
||||
appearance: &Appearance,
|
||||
) -> Option<()> {
|
||||
let (_section, symbol) = obj.0.section_symbol(symbol_ref);
|
||||
|
||||
if let Some(extab_entry) = find_extab_entry(&obj.0, symbol) {
|
||||
let text = decode_extab(&extab_entry);
|
||||
ui.colored_label(appearance.replace_color, &text);
|
||||
return Some(());
|
||||
}
|
||||
|
||||
None
|
||||
}
|
||||
|
||||
fn extab_ui(
|
||||
ui: &mut Ui,
|
||||
obj: Option<&(ObjInfo, ObjDiff)>,
|
||||
selected_symbol: &SymbolRefByName,
|
||||
appearance: &Appearance,
|
||||
_left: bool,
|
||||
) {
|
||||
ScrollArea::both().auto_shrink([false, false]).show(ui, |ui| {
|
||||
ui.scope(|ui| {
|
||||
ui.style_mut().override_text_style = Some(egui::TextStyle::Monospace);
|
||||
ui.style_mut().wrap = Some(false);
|
||||
|
||||
let symbol = obj.and_then(|(obj, _)| find_symbol(obj, selected_symbol));
|
||||
|
||||
if let (Some(object), Some(symbol_ref)) = (obj, symbol) {
|
||||
extab_text_ui(ui, object, symbol_ref, appearance);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
pub fn extab_diff_ui(ui: &mut egui::Ui, state: &mut DiffViewState, appearance: &Appearance) {
|
||||
let (Some(result), Some(selected_symbol)) = (&state.build, &state.symbol_state.selected_symbol)
|
||||
else {
|
||||
return;
|
||||
};
|
||||
|
||||
// Header
|
||||
let available_width = ui.available_width();
|
||||
let column_width = available_width / 2.0;
|
||||
ui.allocate_ui_with_layout(
|
||||
Vec2 { x: available_width, y: 100.0 },
|
||||
Layout::left_to_right(Align::Min),
|
||||
|ui| {
|
||||
// Left column
|
||||
ui.allocate_ui_with_layout(
|
||||
Vec2 { x: column_width, y: 100.0 },
|
||||
Layout::top_down(Align::Min),
|
||||
|ui| {
|
||||
ui.set_width(column_width);
|
||||
|
||||
ui.horizontal(|ui| {
|
||||
if ui.button("⏴ Back").clicked() {
|
||||
state.current_view = View::SymbolDiff;
|
||||
}
|
||||
});
|
||||
|
||||
let name = selected_symbol
|
||||
.demangled_symbol_name
|
||||
.as_deref()
|
||||
.unwrap_or(&selected_symbol.symbol_name);
|
||||
let mut job = LayoutJob::simple(
|
||||
name.to_string(),
|
||||
appearance.code_font.clone(),
|
||||
appearance.highlight_color,
|
||||
column_width,
|
||||
);
|
||||
job.wrap.break_anywhere = true;
|
||||
job.wrap.max_rows = 1;
|
||||
ui.label(job);
|
||||
|
||||
ui.scope(|ui| {
|
||||
ui.style_mut().override_text_style = Some(egui::TextStyle::Monospace);
|
||||
ui.label("Diff target:");
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
// Right column
|
||||
ui.allocate_ui_with_layout(
|
||||
Vec2 { x: column_width, y: 100.0 },
|
||||
Layout::top_down(Align::Min),
|
||||
|ui| {
|
||||
ui.set_width(column_width);
|
||||
|
||||
ui.horizontal(|ui| {
|
||||
if ui
|
||||
.add_enabled(!state.build_running, egui::Button::new("Build"))
|
||||
.clicked()
|
||||
{
|
||||
state.queue_build = true;
|
||||
}
|
||||
ui.scope(|ui| {
|
||||
ui.style_mut().override_text_style = Some(egui::TextStyle::Monospace);
|
||||
ui.style_mut().wrap = Some(false);
|
||||
if state.build_running {
|
||||
ui.colored_label(appearance.replace_color, "Building…");
|
||||
} else {
|
||||
ui.label("Last built:");
|
||||
let format =
|
||||
format_description::parse("[hour]:[minute]:[second]").unwrap();
|
||||
ui.label(
|
||||
result
|
||||
.time
|
||||
.to_offset(appearance.utc_offset)
|
||||
.format(&format)
|
||||
.unwrap(),
|
||||
);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
ui.scope(|ui| {
|
||||
ui.style_mut().override_text_style = Some(egui::TextStyle::Monospace);
|
||||
if let Some(match_percent) = result
|
||||
.second_obj
|
||||
.as_ref()
|
||||
.and_then(|(obj, diff)| {
|
||||
find_symbol(obj, selected_symbol).map(|sref| {
|
||||
&diff.sections[sref.section_idx].symbols[sref.symbol_idx]
|
||||
})
|
||||
})
|
||||
.and_then(|symbol| symbol.match_percent)
|
||||
{
|
||||
ui.colored_label(
|
||||
match_color_for_symbol(match_percent, appearance),
|
||||
&format!("{match_percent:.0}%"),
|
||||
);
|
||||
} else {
|
||||
ui.colored_label(appearance.replace_color, "Missing");
|
||||
}
|
||||
ui.label("Diff base:");
|
||||
});
|
||||
},
|
||||
);
|
||||
},
|
||||
);
|
||||
ui.separator();
|
||||
|
||||
// Table
|
||||
StripBuilder::new(ui).size(Size::remainder()).vertical(|mut strip| {
|
||||
strip.strip(|builder| {
|
||||
builder.sizes(Size::remainder(), 2).horizontal(|mut strip| {
|
||||
strip.cell(|ui| {
|
||||
extab_ui(ui, result.first_obj.as_ref(), selected_symbol, appearance, true);
|
||||
});
|
||||
strip.cell(|ui| {
|
||||
extab_ui(ui, result.second_obj.as_ref(), selected_symbol, appearance, false);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
@@ -216,8 +216,11 @@ fn diff_text_ui(
|
||||
base_color = appearance.diff_colors[diff.idx % appearance.diff_colors.len()]
|
||||
}
|
||||
}
|
||||
DiffText::BranchDest(addr) => {
|
||||
DiffText::BranchDest(addr, diff) => {
|
||||
label_text = format!("{addr:x}");
|
||||
if let Some(diff) = diff {
|
||||
base_color = appearance.diff_colors[diff.idx % appearance.diff_colors.len()]
|
||||
}
|
||||
}
|
||||
DiffText::Symbol(sym) => {
|
||||
let name = sym.demangled_name.as_ref().unwrap_or(&sym.name);
|
||||
|
||||
158
objdiff-gui/src/views/graphics.rs
Normal file
158
objdiff-gui/src/views/graphics.rs
Normal file
@@ -0,0 +1,158 @@
|
||||
use std::{
|
||||
fs::File,
|
||||
path::{Path, PathBuf},
|
||||
};
|
||||
|
||||
use anyhow::Result;
|
||||
use egui::{text::LayoutJob, Context, FontId, RichText, TextFormat, TextStyle, Window};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use strum::{EnumIter, EnumMessage, IntoEnumIterator};
|
||||
|
||||
use crate::views::{appearance::Appearance, frame_history::FrameHistory};
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct GraphicsViewState {
|
||||
pub active_backend: String,
|
||||
pub active_device: String,
|
||||
pub graphics_config: GraphicsConfig,
|
||||
pub graphics_config_path: Option<PathBuf>,
|
||||
pub should_relaunch: bool,
|
||||
}
|
||||
|
||||
#[derive(
|
||||
Copy, Clone, Debug, Default, PartialEq, Eq, EnumIter, EnumMessage, Serialize, Deserialize,
|
||||
)]
|
||||
pub enum GraphicsBackend {
|
||||
#[default]
|
||||
#[strum(message = "Auto")]
|
||||
Auto,
|
||||
#[strum(message = "Vulkan")]
|
||||
Vulkan,
|
||||
#[strum(message = "Metal")]
|
||||
Metal,
|
||||
#[strum(message = "DirectX 12")]
|
||||
Dx12,
|
||||
#[strum(message = "OpenGL")]
|
||||
OpenGL,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Default, serde::Deserialize, serde::Serialize)]
|
||||
pub struct GraphicsConfig {
|
||||
#[serde(default)]
|
||||
pub desired_backend: GraphicsBackend,
|
||||
}
|
||||
|
||||
pub fn load_graphics_config(path: &Path) -> Result<Option<GraphicsConfig>> {
|
||||
if !path.exists() {
|
||||
return Ok(None);
|
||||
}
|
||||
let file = File::open(path)?;
|
||||
let config: GraphicsConfig = ron::de::from_reader(file)?;
|
||||
Ok(Some(config))
|
||||
}
|
||||
|
||||
pub fn save_graphics_config(path: &Path, config: &GraphicsConfig) -> Result<()> {
|
||||
let file = File::create(path)?;
|
||||
ron::ser::to_writer(file, config)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
impl GraphicsBackend {
|
||||
pub fn is_supported(&self) -> bool {
|
||||
match self {
|
||||
GraphicsBackend::Auto => true,
|
||||
GraphicsBackend::Vulkan => {
|
||||
cfg!(all(feature = "wgpu", any(target_os = "windows", target_os = "linux")))
|
||||
}
|
||||
GraphicsBackend::Metal => cfg!(all(feature = "wgpu", target_os = "macos")),
|
||||
GraphicsBackend::Dx12 => cfg!(all(feature = "wgpu", target_os = "windows")),
|
||||
GraphicsBackend::OpenGL => true,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn graphics_window(
|
||||
ctx: &Context,
|
||||
show: &mut bool,
|
||||
frame_history: &mut FrameHistory,
|
||||
state: &mut GraphicsViewState,
|
||||
appearance: &Appearance,
|
||||
) {
|
||||
Window::new("Graphics").open(show).show(ctx, |ui| {
|
||||
ui.label("Graphics backend:");
|
||||
ui.label(
|
||||
RichText::new(&state.active_backend)
|
||||
.color(appearance.emphasized_text_color)
|
||||
.text_style(TextStyle::Monospace),
|
||||
);
|
||||
ui.label("Graphics device:");
|
||||
ui.label(
|
||||
RichText::new(&state.active_device)
|
||||
.color(appearance.emphasized_text_color)
|
||||
.text_style(TextStyle::Monospace),
|
||||
);
|
||||
ui.label(format!("FPS: {:.1}", frame_history.fps()));
|
||||
|
||||
ui.separator();
|
||||
let mut job = LayoutJob::default();
|
||||
job.append(
|
||||
"WARNING: ",
|
||||
0.0,
|
||||
TextFormat::simple(appearance.ui_font.clone(), appearance.delete_color),
|
||||
);
|
||||
job.append(
|
||||
"Changing the graphics backend may cause the application\nto no longer start or display correctly. Use with caution!",
|
||||
0.0,
|
||||
TextFormat::simple(appearance.ui_font.clone(), appearance.emphasized_text_color),
|
||||
);
|
||||
if let Some(config_path) = &state.graphics_config_path {
|
||||
job.append(
|
||||
"\n\nDelete the following file to reset:\n",
|
||||
0.0,
|
||||
TextFormat::simple(appearance.ui_font.clone(), appearance.emphasized_text_color),
|
||||
);
|
||||
job.append(
|
||||
config_path.to_string_lossy().as_ref(),
|
||||
0.0,
|
||||
TextFormat::simple(
|
||||
FontId {
|
||||
family: appearance.code_font.family.clone(),
|
||||
size: appearance.ui_font.size,
|
||||
},
|
||||
appearance.emphasized_text_color,
|
||||
),
|
||||
);
|
||||
}
|
||||
job.append(
|
||||
"\n\nChanging the graphics backend will restart the application.",
|
||||
0.0,
|
||||
TextFormat::simple(appearance.ui_font.clone(), appearance.replace_color),
|
||||
);
|
||||
ui.label(job);
|
||||
|
||||
ui.add_enabled_ui(state.graphics_config_path.is_some(), |ui| {
|
||||
ui.horizontal(|ui| {
|
||||
ui.label("Desired backend:");
|
||||
for backend in GraphicsBackend::iter().filter(GraphicsBackend::is_supported) {
|
||||
let selected = state.graphics_config.desired_backend == backend;
|
||||
if ui.selectable_label(selected, backend.get_message().unwrap()).clicked() {
|
||||
let prev_backend = state.graphics_config.desired_backend;
|
||||
state.graphics_config.desired_backend = backend;
|
||||
match save_graphics_config(
|
||||
state.graphics_config_path.as_ref().unwrap(),
|
||||
&state.graphics_config,
|
||||
) {
|
||||
Ok(()) => {
|
||||
state.should_relaunch = true;
|
||||
}
|
||||
Err(e) => {
|
||||
log::error!("Failed to save graphics config: {:?}", e);
|
||||
state.graphics_config.desired_backend = prev_backend;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
@@ -5,10 +5,13 @@ pub(crate) mod config;
|
||||
pub(crate) mod data_diff;
|
||||
pub(crate) mod debug;
|
||||
pub(crate) mod demangle;
|
||||
pub(crate) mod extab_diff;
|
||||
pub(crate) mod file;
|
||||
pub(crate) mod frame_history;
|
||||
pub(crate) mod function_diff;
|
||||
pub(crate) mod graphics;
|
||||
pub(crate) mod jobs;
|
||||
pub(crate) mod rlwinm;
|
||||
pub(crate) mod symbol_diff;
|
||||
|
||||
#[inline]
|
||||
|
||||
34
objdiff-gui/src/views/rlwinm.rs
Normal file
34
objdiff-gui/src/views/rlwinm.rs
Normal file
@@ -0,0 +1,34 @@
|
||||
use egui::TextStyle;
|
||||
|
||||
use crate::views::appearance::Appearance;
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct RlwinmDecodeViewState {
|
||||
pub text: String,
|
||||
}
|
||||
|
||||
pub fn rlwinm_decode_window(
|
||||
ctx: &egui::Context,
|
||||
show: &mut bool,
|
||||
state: &mut RlwinmDecodeViewState,
|
||||
appearance: &Appearance,
|
||||
) {
|
||||
egui::Window::new("Rlwinm Decoder").open(show).show(ctx, |ui| {
|
||||
ui.text_edit_singleline(&mut state.text);
|
||||
ui.add_space(10.0);
|
||||
if let Some(demangled) = rlwinmdec::decode(&state.text) {
|
||||
ui.scope(|ui| {
|
||||
ui.style_mut().override_text_style = Some(TextStyle::Monospace);
|
||||
ui.colored_label(appearance.replace_color, &demangled);
|
||||
});
|
||||
if ui.button("Copy").clicked() {
|
||||
ui.output_mut(|output| output.copied_text = demangled);
|
||||
}
|
||||
} else {
|
||||
ui.scope(|ui| {
|
||||
ui.style_mut().override_text_style = Some(TextStyle::Monospace);
|
||||
ui.colored_label(appearance.replace_color, "[invalid]");
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -9,6 +9,7 @@ use objdiff_core::{
|
||||
diff::{ObjDiff, ObjSymbolDiff},
|
||||
obj::{ObjInfo, ObjSection, ObjSectionKind, ObjSymbol, ObjSymbolFlags, SymbolRef},
|
||||
};
|
||||
use regex::{Regex, RegexBuilder};
|
||||
|
||||
use crate::{
|
||||
app::AppConfigRef,
|
||||
@@ -33,6 +34,7 @@ pub enum View {
|
||||
SymbolDiff,
|
||||
FunctionDiff,
|
||||
DataDiff,
|
||||
ExtabDiff,
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
@@ -43,6 +45,7 @@ pub struct DiffViewState {
|
||||
pub symbol_state: SymbolViewState,
|
||||
pub function_state: FunctionViewState,
|
||||
pub search: String,
|
||||
pub search_regex: Option<Regex>,
|
||||
pub queue_build: bool,
|
||||
pub build_running: bool,
|
||||
pub scratch_available: bool,
|
||||
@@ -57,6 +60,7 @@ pub struct SymbolViewState {
|
||||
pub reverse_fn_order: bool,
|
||||
pub disable_reverse_fn_order: bool,
|
||||
pub show_hidden_symbols: bool,
|
||||
pub queue_extab_decode: bool,
|
||||
}
|
||||
|
||||
impl DiffViewState {
|
||||
@@ -131,7 +135,12 @@ pub fn match_color_for_symbol(match_percent: f32, appearance: &Appearance) -> Co
|
||||
}
|
||||
}
|
||||
|
||||
fn symbol_context_menu_ui(ui: &mut Ui, symbol: &ObjSymbol) {
|
||||
fn symbol_context_menu_ui(
|
||||
ui: &mut Ui,
|
||||
state: &mut SymbolViewState,
|
||||
symbol: &ObjSymbol,
|
||||
section: Option<&ObjSection>,
|
||||
) {
|
||||
ui.scope(|ui| {
|
||||
ui.style_mut().override_text_style = Some(egui::TextStyle::Monospace);
|
||||
ui.style_mut().wrap = Some(false);
|
||||
@@ -152,6 +161,17 @@ fn symbol_context_menu_ui(ui: &mut Ui, symbol: &ObjSymbol) {
|
||||
ui.close_menu();
|
||||
}
|
||||
}
|
||||
if let Some(section) = section {
|
||||
if symbol.has_extab && ui.button("Decode exception table").clicked() {
|
||||
state.queue_extab_decode = true;
|
||||
state.selected_symbol = Some(SymbolRefByName {
|
||||
symbol_name: symbol.name.clone(),
|
||||
demangled_symbol_name: symbol.demangled_name.clone(),
|
||||
section_name: section.name.clone(),
|
||||
});
|
||||
ui.close_menu();
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -173,6 +193,20 @@ fn symbol_hover_ui(ui: &mut Ui, symbol: &ObjSymbol, appearance: &Appearance) {
|
||||
if let Some(address) = symbol.virtual_address {
|
||||
ui.colored_label(appearance.replace_color, format!("Virtual address: {:#x}", address));
|
||||
}
|
||||
if symbol.has_extab {
|
||||
if let (Some(extab_name), Some(extabindex_name)) =
|
||||
(&symbol.extab_name, &symbol.extabindex_name)
|
||||
{
|
||||
ui.colored_label(
|
||||
appearance.highlight_color,
|
||||
format!("Extab Symbol: {}", extab_name),
|
||||
);
|
||||
ui.colored_label(
|
||||
appearance.highlight_color,
|
||||
format!("Extabindex Symbol: {}", extabindex_name),
|
||||
);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -199,21 +233,28 @@ fn symbol_ui(
|
||||
{
|
||||
selected = symbol_diff.symbol_ref == sym_ref;
|
||||
}
|
||||
write_text("[", appearance.text_color, &mut job, appearance.code_font.clone());
|
||||
if symbol.flags.0.contains(ObjSymbolFlags::Common) {
|
||||
write_text("c", appearance.replace_color, &mut job, appearance.code_font.clone());
|
||||
} else if symbol.flags.0.contains(ObjSymbolFlags::Global) {
|
||||
write_text("g", appearance.insert_color, &mut job, appearance.code_font.clone());
|
||||
} else if symbol.flags.0.contains(ObjSymbolFlags::Local) {
|
||||
write_text("l", appearance.text_color, &mut job, appearance.code_font.clone());
|
||||
if !symbol.flags.0.is_empty() {
|
||||
write_text("[", appearance.text_color, &mut job, appearance.code_font.clone());
|
||||
if symbol.flags.0.contains(ObjSymbolFlags::Common) {
|
||||
write_text("c", appearance.replace_color, &mut job, appearance.code_font.clone());
|
||||
} else if symbol.flags.0.contains(ObjSymbolFlags::Global) {
|
||||
write_text("g", appearance.insert_color, &mut job, appearance.code_font.clone());
|
||||
} else if symbol.flags.0.contains(ObjSymbolFlags::Local) {
|
||||
write_text("l", appearance.text_color, &mut job, appearance.code_font.clone());
|
||||
}
|
||||
if symbol.flags.0.contains(ObjSymbolFlags::Weak) {
|
||||
write_text("w", appearance.text_color, &mut job, appearance.code_font.clone());
|
||||
}
|
||||
if symbol.flags.0.contains(ObjSymbolFlags::Hidden) {
|
||||
write_text(
|
||||
"h",
|
||||
appearance.deemphasized_text_color,
|
||||
&mut job,
|
||||
appearance.code_font.clone(),
|
||||
);
|
||||
}
|
||||
write_text("] ", appearance.text_color, &mut job, appearance.code_font.clone());
|
||||
}
|
||||
if symbol.flags.0.contains(ObjSymbolFlags::Weak) {
|
||||
write_text("w", appearance.text_color, &mut job, appearance.code_font.clone());
|
||||
}
|
||||
if symbol.flags.0.contains(ObjSymbolFlags::Hidden) {
|
||||
write_text("h", appearance.deemphasized_text_color, &mut job, appearance.code_font.clone());
|
||||
}
|
||||
write_text("] ", appearance.text_color, &mut job, appearance.code_font.clone());
|
||||
if let Some(match_percent) = symbol_diff.match_percent {
|
||||
write_text("(", appearance.text_color, &mut job, appearance.code_font.clone());
|
||||
write_text(
|
||||
@@ -228,7 +269,7 @@ fn symbol_ui(
|
||||
let response = SelectableLabel::new(selected, job)
|
||||
.ui(ui)
|
||||
.on_hover_ui_at_pointer(|ui| symbol_hover_ui(ui, symbol, appearance));
|
||||
response.context_menu(|ui| symbol_context_menu_ui(ui, symbol));
|
||||
response.context_menu(|ui| symbol_context_menu_ui(ui, state, symbol, section));
|
||||
if response.clicked() {
|
||||
if let Some(section) = section {
|
||||
if section.kind == ObjSectionKind::Code {
|
||||
@@ -258,17 +299,23 @@ fn symbol_ui(
|
||||
(None, None)
|
||||
};
|
||||
}
|
||||
|
||||
//If the decode extab context menu option was clicked, switch to the extab view
|
||||
if state.queue_extab_decode {
|
||||
ret = Some(View::ExtabDiff);
|
||||
state.queue_extab_decode = false;
|
||||
}
|
||||
|
||||
ret
|
||||
}
|
||||
|
||||
fn symbol_matches_search(symbol: &ObjSymbol, search_str: &str) -> bool {
|
||||
search_str.is_empty()
|
||||
|| symbol.name.contains(search_str)
|
||||
|| symbol
|
||||
.demangled_name
|
||||
.as_ref()
|
||||
.map(|s| s.to_ascii_lowercase().contains(search_str))
|
||||
.unwrap_or(false)
|
||||
fn symbol_matches_search(symbol: &ObjSymbol, search_regex: Option<&Regex>) -> bool {
|
||||
if let Some(search_regex) = search_regex {
|
||||
search_regex.is_match(&symbol.name)
|
||||
|| symbol.demangled_name.as_ref().map(|s| search_regex.is_match(s)).unwrap_or(false)
|
||||
} else {
|
||||
true
|
||||
}
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
@@ -276,7 +323,7 @@ fn symbol_list_ui(
|
||||
ui: &mut Ui,
|
||||
obj: &(ObjInfo, ObjDiff),
|
||||
state: &mut SymbolViewState,
|
||||
lower_search: &str,
|
||||
search_regex: Option<&Regex>,
|
||||
appearance: &Appearance,
|
||||
left: bool,
|
||||
) -> Option<View> {
|
||||
@@ -289,6 +336,9 @@ fn symbol_list_ui(
|
||||
if !obj.0.common.is_empty() {
|
||||
CollapsingHeader::new(".comm").default_open(true).show(ui, |ui| {
|
||||
for (symbol, symbol_diff) in obj.0.common.iter().zip(&obj.1.common) {
|
||||
if !symbol_matches_search(symbol, search_regex) {
|
||||
continue;
|
||||
}
|
||||
ret = ret.or(symbol_ui(
|
||||
ui,
|
||||
symbol,
|
||||
@@ -336,7 +386,7 @@ fn symbol_list_ui(
|
||||
for (symbol, symbol_diff) in
|
||||
section.symbols.iter().zip(§ion_diff.symbols).rev()
|
||||
{
|
||||
if !symbol_matches_search(symbol, lower_search) {
|
||||
if !symbol_matches_search(symbol, search_regex) {
|
||||
continue;
|
||||
}
|
||||
ret = ret.or(symbol_ui(
|
||||
@@ -353,7 +403,7 @@ fn symbol_list_ui(
|
||||
for (symbol, symbol_diff) in
|
||||
section.symbols.iter().zip(§ion_diff.symbols)
|
||||
{
|
||||
if !symbol_matches_search(symbol, lower_search) {
|
||||
if !symbol_matches_search(symbol, search_regex) {
|
||||
continue;
|
||||
}
|
||||
ret = ret.or(symbol_ui(
|
||||
@@ -407,7 +457,7 @@ fn missing_obj_ui(ui: &mut Ui, appearance: &Appearance) {
|
||||
}
|
||||
|
||||
pub fn symbol_diff_ui(ui: &mut Ui, state: &mut DiffViewState, appearance: &Appearance) {
|
||||
let DiffViewState { build, current_view, symbol_state, search, .. } = state;
|
||||
let DiffViewState { build, current_view, symbol_state, search, search_regex, .. } = state;
|
||||
let Some(result) = build else {
|
||||
return;
|
||||
};
|
||||
@@ -442,7 +492,17 @@ pub fn symbol_diff_ui(ui: &mut Ui, state: &mut DiffViewState, appearance: &Appea
|
||||
}
|
||||
});
|
||||
|
||||
TextEdit::singleline(search).hint_text("Filter symbols").ui(ui);
|
||||
if TextEdit::singleline(search).hint_text("Filter symbols").ui(ui).changed() {
|
||||
if search.is_empty() {
|
||||
*search_regex = None;
|
||||
} else if let Ok(regex) =
|
||||
RegexBuilder::new(search).case_insensitive(true).build()
|
||||
{
|
||||
*search_regex = Some(regex);
|
||||
} else {
|
||||
*search_regex = None;
|
||||
}
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
@@ -480,7 +540,6 @@ pub fn symbol_diff_ui(ui: &mut Ui, state: &mut DiffViewState, appearance: &Appea
|
||||
|
||||
// Table
|
||||
let mut ret = None;
|
||||
let lower_search = search.to_ascii_lowercase();
|
||||
StripBuilder::new(ui).size(Size::remainder()).vertical(|mut strip| {
|
||||
strip.strip(|builder| {
|
||||
builder.sizes(Size::remainder(), 2).horizontal(|mut strip| {
|
||||
@@ -492,7 +551,7 @@ pub fn symbol_diff_ui(ui: &mut Ui, state: &mut DiffViewState, appearance: &Appea
|
||||
ui,
|
||||
obj,
|
||||
symbol_state,
|
||||
&lower_search,
|
||||
search_regex.as_ref(),
|
||||
appearance,
|
||||
true,
|
||||
));
|
||||
@@ -512,7 +571,7 @@ pub fn symbol_diff_ui(ui: &mut Ui, state: &mut DiffViewState, appearance: &Appea
|
||||
ui,
|
||||
obj,
|
||||
symbol_state,
|
||||
&lower_search,
|
||||
search_regex.as_ref(),
|
||||
appearance,
|
||||
false,
|
||||
));
|
||||
|
||||
4
objdiff-wasm/.gitignore
vendored
Normal file
4
objdiff-wasm/.gitignore
vendored
Normal file
@@ -0,0 +1,4 @@
|
||||
dist/
|
||||
gen/
|
||||
node_modules/
|
||||
pkg/
|
||||
28
objdiff-wasm/eslint.config.js
Normal file
28
objdiff-wasm/eslint.config.js
Normal file
@@ -0,0 +1,28 @@
|
||||
import globals from "globals";
|
||||
import pluginJs from "@eslint/js";
|
||||
import tseslint from "typescript-eslint";
|
||||
|
||||
export default [
|
||||
{files: ["**/*.{js,mjs,cjs,ts}"]},
|
||||
{languageOptions: {globals: globals.browser}},
|
||||
pluginJs.configs.recommended,
|
||||
...tseslint.configs.recommended,
|
||||
{
|
||||
rules: {
|
||||
"semi": [2, "always"],
|
||||
"@typescript-eslint/no-unused-vars": [
|
||||
"error",
|
||||
// https://typescript-eslint.io/rules/no-unused-vars/#benefits-over-typescript
|
||||
{
|
||||
"args": "all",
|
||||
"argsIgnorePattern": "^_",
|
||||
"caughtErrors": "all",
|
||||
"caughtErrorsIgnorePattern": "^_",
|
||||
"destructuredArrayIgnorePattern": "^_",
|
||||
"varsIgnorePattern": "^_",
|
||||
"ignoreRestSiblings": true
|
||||
},
|
||||
],
|
||||
}
|
||||
},
|
||||
];
|
||||
3519
objdiff-wasm/package-lock.json
generated
Normal file
3519
objdiff-wasm/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
39
objdiff-wasm/package.json
Normal file
39
objdiff-wasm/package.json
Normal file
@@ -0,0 +1,39 @@
|
||||
{
|
||||
"name": "objdiff-wasm",
|
||||
"version": "2.0.0-beta.10",
|
||||
"description": "A local diffing tool for decompilation projects.",
|
||||
"author": {
|
||||
"name": "Luke Street",
|
||||
"email": "luke@street.dev"
|
||||
},
|
||||
"license": "MIT OR Apache-2.0",
|
||||
"type": "module",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git+https://github.com/encounter/objdiff.git"
|
||||
},
|
||||
"files": [
|
||||
"dist/*"
|
||||
],
|
||||
"main": "dist/main.js",
|
||||
"types": "dist/main.d.ts",
|
||||
"scripts": {
|
||||
"build": "tsup",
|
||||
"build:all": "npm run build:wasm && npm run build:proto && npm run build",
|
||||
"build:proto": "protoc --ts_out=gen --ts_opt add_pb_suffix,eslint_disable,ts_nocheck,use_proto_field_name --proto_path=../objdiff-core/protos ../objdiff-core/protos/*.proto",
|
||||
"build:wasm": "cd ../objdiff-core && wasm-pack build --out-dir ../objdiff-wasm/pkg --target web -- --features arm,dwarf,ppc,x86,wasm"
|
||||
},
|
||||
"dependencies": {
|
||||
"@protobuf-ts/runtime": "^2.9.4"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@eslint/js": "^9.9.0",
|
||||
"@protobuf-ts/plugin": "^2.9.4",
|
||||
"@types/node": "^22.4.1",
|
||||
"esbuild": "^0.23.1",
|
||||
"eslint": "^9.9.0",
|
||||
"globals": "^15.9.0",
|
||||
"tsup": "^8.2.4",
|
||||
"typescript-eslint": "^8.2.0"
|
||||
}
|
||||
}
|
||||
227
objdiff-wasm/src/main.ts
Normal file
227
objdiff-wasm/src/main.ts
Normal file
@@ -0,0 +1,227 @@
|
||||
import {ArgumentValue, DiffResult, InstructionDiff, RelocationTarget} from "../gen/diff_pb";
|
||||
import type {
|
||||
ArmArchVersion,
|
||||
ArmR9Usage,
|
||||
DiffObjConfig,
|
||||
MipsAbi,
|
||||
MipsInstrCategory,
|
||||
X86Formatter
|
||||
} from '../pkg';
|
||||
import {AnyHandlerData, InMessage, OutMessage} from './worker';
|
||||
|
||||
// Export wasm types
|
||||
export {ArmArchVersion, ArmR9Usage, MipsAbi, MipsInstrCategory, X86Formatter, DiffObjConfig};
|
||||
|
||||
// Export protobuf types
|
||||
export * from '../gen/diff_pb';
|
||||
|
||||
interface PromiseCallbacks<T> {
|
||||
start: number;
|
||||
resolve: (value: T | PromiseLike<T>) => void;
|
||||
reject: (reason?: string) => void;
|
||||
}
|
||||
|
||||
let workerInit = false;
|
||||
let workerCallbacks: PromiseCallbacks<Worker>;
|
||||
const workerReady = new Promise<Worker>((resolve, reject) => {
|
||||
workerCallbacks = {start: performance.now(), resolve, reject};
|
||||
});
|
||||
|
||||
export async function initialize(data?: {
|
||||
workerUrl?: string | URL,
|
||||
wasmUrl?: string | URL, // Relative to worker URL
|
||||
}): Promise<Worker> {
|
||||
if (workerInit) {
|
||||
return workerReady;
|
||||
}
|
||||
workerInit = true;
|
||||
let {workerUrl, wasmUrl} = data || {};
|
||||
if (!workerUrl) {
|
||||
try {
|
||||
// Bundlers will convert this into an asset URL
|
||||
workerUrl = new URL('./worker.js', import.meta.url);
|
||||
} catch (_) {
|
||||
workerUrl = 'worker.js';
|
||||
}
|
||||
}
|
||||
if (!wasmUrl) {
|
||||
try {
|
||||
// Bundlers will convert this into an asset URL
|
||||
wasmUrl = new URL('./objdiff_core_bg.wasm', import.meta.url);
|
||||
} catch (_) {
|
||||
wasmUrl = 'objdiff_core_bg.js';
|
||||
}
|
||||
}
|
||||
const worker = new Worker(workerUrl, {
|
||||
name: 'objdiff',
|
||||
type: 'module',
|
||||
});
|
||||
worker.onmessage = onMessage;
|
||||
worker.onerror = (event) => {
|
||||
console.error("Worker error", event);
|
||||
workerCallbacks.reject("Worker failed to initialize, wrong URL?");
|
||||
};
|
||||
defer<void>({
|
||||
type: 'init',
|
||||
// URL can't be sent directly
|
||||
wasmUrl: wasmUrl.toString(),
|
||||
}, worker).then(() => {
|
||||
workerCallbacks.resolve(worker);
|
||||
}, (e) => {
|
||||
workerCallbacks.reject(e);
|
||||
});
|
||||
return workerReady;
|
||||
}
|
||||
|
||||
let globalMessageId = 0;
|
||||
const messageCallbacks = new Map<number, PromiseCallbacks<never>>();
|
||||
|
||||
function onMessage(event: MessageEvent<OutMessage>) {
|
||||
switch (event.data.type) {
|
||||
case 'result': {
|
||||
const {result, error, messageId} = event.data;
|
||||
const callbacks = messageCallbacks.get(messageId);
|
||||
if (callbacks) {
|
||||
const end = performance.now();
|
||||
console.debug(`Message ${messageId} took ${end - callbacks.start}ms`);
|
||||
messageCallbacks.delete(messageId);
|
||||
if (error != null) {
|
||||
callbacks.reject(error);
|
||||
} else {
|
||||
callbacks.resolve(result as never);
|
||||
}
|
||||
} else {
|
||||
console.warn(`Unknown message ID ${messageId}`);
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function defer<T>(message: AnyHandlerData, worker?: Worker): Promise<T> {
|
||||
worker = worker || await initialize();
|
||||
const messageId = globalMessageId++;
|
||||
const promise = new Promise<T>((resolve, reject) => {
|
||||
messageCallbacks.set(messageId, {start: performance.now(), resolve, reject});
|
||||
});
|
||||
worker.postMessage({
|
||||
...message,
|
||||
messageId
|
||||
} as InMessage);
|
||||
return promise;
|
||||
}
|
||||
|
||||
export async function runDiff(left: Uint8Array | undefined, right: Uint8Array | undefined, config?: DiffObjConfig): Promise<DiffResult> {
|
||||
const data = await defer<Uint8Array>({
|
||||
type: 'run_diff_proto',
|
||||
left,
|
||||
right,
|
||||
config
|
||||
});
|
||||
const parseStart = performance.now();
|
||||
const result = DiffResult.fromBinary(data, {readUnknownField: false});
|
||||
const end = performance.now();
|
||||
console.debug(`Parsing message took ${end - parseStart}ms`);
|
||||
return result;
|
||||
}
|
||||
|
||||
export type DiffText =
|
||||
DiffTextBasic
|
||||
| DiffTextBasicColor
|
||||
| DiffTextAddress
|
||||
| DiffTextLine
|
||||
| DiffTextOpcode
|
||||
| DiffTextArgument
|
||||
| DiffTextSymbol
|
||||
| DiffTextBranchDest
|
||||
| DiffTextSpacing;
|
||||
|
||||
type DiffTextBase = {
|
||||
diff_index?: number,
|
||||
};
|
||||
export type DiffTextBasic = DiffTextBase & {
|
||||
type: 'basic',
|
||||
text: string,
|
||||
};
|
||||
export type DiffTextBasicColor = DiffTextBase & {
|
||||
type: 'basic_color',
|
||||
text: string,
|
||||
index: number,
|
||||
};
|
||||
export type DiffTextAddress = DiffTextBase & {
|
||||
type: 'address',
|
||||
address: bigint,
|
||||
};
|
||||
export type DiffTextLine = DiffTextBase & {
|
||||
type: 'line',
|
||||
line_number: number,
|
||||
};
|
||||
export type DiffTextOpcode = DiffTextBase & {
|
||||
type: 'opcode',
|
||||
mnemonic: string,
|
||||
opcode: number,
|
||||
};
|
||||
export type DiffTextArgument = DiffTextBase & {
|
||||
type: 'argument',
|
||||
value: ArgumentValue,
|
||||
};
|
||||
export type DiffTextSymbol = DiffTextBase & {
|
||||
type: 'symbol',
|
||||
target: RelocationTarget,
|
||||
};
|
||||
export type DiffTextBranchDest = DiffTextBase & {
|
||||
type: 'branch_dest',
|
||||
address: bigint,
|
||||
};
|
||||
export type DiffTextSpacing = DiffTextBase & {
|
||||
type: 'spacing',
|
||||
count: number,
|
||||
};
|
||||
|
||||
// Native JavaScript implementation of objdiff_core::diff::display::display_diff
|
||||
export function displayDiff(diff: InstructionDiff, baseAddr: bigint, cb: (text: DiffText) => void) {
|
||||
const ins = diff.instruction;
|
||||
if (!ins) {
|
||||
return;
|
||||
}
|
||||
if (ins.line_number != null) {
|
||||
cb({type: 'line', line_number: ins.line_number});
|
||||
}
|
||||
cb({type: 'address', address: ins.address - baseAddr});
|
||||
if (diff.branch_from) {
|
||||
cb({type: 'basic_color', text: ' ~> ', index: diff.branch_from.branch_index});
|
||||
} else {
|
||||
cb({type: 'spacing', count: 4});
|
||||
}
|
||||
cb({type: 'opcode', mnemonic: ins.mnemonic, opcode: ins.opcode});
|
||||
for (let i = 0; i < ins.arguments.length; i++) {
|
||||
if (i === 0) {
|
||||
cb({type: 'spacing', count: 1});
|
||||
}
|
||||
const arg = ins.arguments[i].value;
|
||||
const diff_index = diff.arg_diff[i]?.diff_index;
|
||||
switch (arg.oneofKind) {
|
||||
case "plain_text":
|
||||
cb({type: 'basic', text: arg.plain_text, diff_index});
|
||||
break;
|
||||
case "argument":
|
||||
cb({type: 'argument', value: arg.argument, diff_index});
|
||||
break;
|
||||
case "relocation": {
|
||||
const reloc = ins.relocation!;
|
||||
cb({type: 'symbol', target: reloc.target!, diff_index});
|
||||
break;
|
||||
}
|
||||
case "branch_dest":
|
||||
if (arg.branch_dest < baseAddr) {
|
||||
cb({type: 'basic', text: '<unknown>', diff_index});
|
||||
} else {
|
||||
cb({type: 'branch_dest', address: arg.branch_dest - baseAddr, diff_index});
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (diff.branch_to) {
|
||||
cb({type: 'basic_color', text: ' ~> ', index: diff.branch_to.branch_index});
|
||||
}
|
||||
}
|
||||
93
objdiff-wasm/src/worker.ts
Normal file
93
objdiff-wasm/src/worker.ts
Normal file
@@ -0,0 +1,93 @@
|
||||
import wasmInit, * as exports from '../pkg';
|
||||
|
||||
const handlers = {
|
||||
init: init,
|
||||
// run_diff_json: run_diff_json,
|
||||
run_diff_proto: run_diff_proto,
|
||||
} as const;
|
||||
type ExtractData<T> = T extends (arg: infer U) => Promise<unknown> ? U : never;
|
||||
type HandlerData = {
|
||||
[K in keyof typeof handlers]: { type: K } & ExtractData<typeof handlers[K]>;
|
||||
};
|
||||
|
||||
let wasmReady: Promise<void> | null = null;
|
||||
|
||||
async function init({wasmUrl}: { wasmUrl?: string }): Promise<void> {
|
||||
if (wasmReady != null) {
|
||||
throw new Error('Already initialized');
|
||||
}
|
||||
wasmReady = wasmInit({module_or_path: wasmUrl})
|
||||
.then(() => {
|
||||
});
|
||||
return wasmReady;
|
||||
}
|
||||
|
||||
async function initIfNeeded() {
|
||||
if (wasmReady == null) {
|
||||
await init({});
|
||||
}
|
||||
return wasmReady;
|
||||
}
|
||||
|
||||
// async function run_diff_json({left, right, config}: {
|
||||
// left: Uint8Array | undefined,
|
||||
// right: Uint8Array | undefined,
|
||||
// config?: exports.DiffObjConfig,
|
||||
// }): Promise<string> {
|
||||
// config = config || exports.default_diff_obj_config();
|
||||
// return exports.run_diff_json(left, right, cfg);
|
||||
// }
|
||||
|
||||
async function run_diff_proto({left, right, config}: {
|
||||
left: Uint8Array | undefined,
|
||||
right: Uint8Array | undefined,
|
||||
config?: exports.DiffObjConfig,
|
||||
}): Promise<Uint8Array> {
|
||||
config = config || {};
|
||||
return exports.run_diff_proto(left, right, config);
|
||||
}
|
||||
|
||||
export type AnyHandlerData = HandlerData[keyof HandlerData];
|
||||
export type InMessage = AnyHandlerData & { messageId: number };
|
||||
|
||||
export type OutMessage = {
|
||||
type: 'result',
|
||||
result: unknown | null,
|
||||
error: string | null,
|
||||
messageId: number,
|
||||
};
|
||||
|
||||
self.onmessage = (event: MessageEvent<InMessage>) => {
|
||||
const data = event.data;
|
||||
const messageId = data?.messageId;
|
||||
(async () => {
|
||||
if (!data) {
|
||||
throw new Error('No data');
|
||||
}
|
||||
const handler = handlers[data.type];
|
||||
if (handler) {
|
||||
if (data.type !== 'init') {
|
||||
await initIfNeeded();
|
||||
}
|
||||
const start = performance.now();
|
||||
const result = await handler(data as never);
|
||||
const end = performance.now();
|
||||
console.debug(`Worker message ${data.messageId} took ${end - start}ms`);
|
||||
self.postMessage({
|
||||
type: 'result',
|
||||
result: result,
|
||||
error: null,
|
||||
messageId,
|
||||
} as OutMessage);
|
||||
} else {
|
||||
throw new Error(`No handler for ${data.type}`);
|
||||
}
|
||||
})().catch(error => {
|
||||
self.postMessage({
|
||||
type: 'result',
|
||||
result: null,
|
||||
error: error.toString(),
|
||||
messageId,
|
||||
} as OutMessage);
|
||||
});
|
||||
};
|
||||
9
objdiff-wasm/tsconfig.json
Normal file
9
objdiff-wasm/tsconfig.json
Normal file
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"esModuleInterop": true,
|
||||
"module": "ES2022",
|
||||
"moduleResolution": "Node",
|
||||
"strict": true,
|
||||
"target": "ES2022",
|
||||
}
|
||||
}
|
||||
33
objdiff-wasm/tsup.config.ts
Normal file
33
objdiff-wasm/tsup.config.ts
Normal file
@@ -0,0 +1,33 @@
|
||||
import {defineConfig} from 'tsup';
|
||||
import fs from 'node:fs/promises';
|
||||
|
||||
export default defineConfig([
|
||||
// Build main library
|
||||
{
|
||||
entry: ['src/main.ts'],
|
||||
clean: true,
|
||||
dts: true,
|
||||
format: 'esm',
|
||||
outDir: 'dist',
|
||||
skipNodeModulesBundle: true,
|
||||
sourcemap: true,
|
||||
splitting: false,
|
||||
target: 'es2022',
|
||||
},
|
||||
// Build web worker
|
||||
{
|
||||
entry: ['src/worker.ts'],
|
||||
clean: true,
|
||||
dts: true,
|
||||
format: 'esm', // type: 'module'
|
||||
minify: true,
|
||||
outDir: 'dist',
|
||||
sourcemap: true,
|
||||
splitting: false,
|
||||
target: 'es2022',
|
||||
// https://github.com/egoist/tsup/issues/278
|
||||
async onSuccess() {
|
||||
await fs.copyFile('pkg/objdiff_core_bg.wasm', 'dist/objdiff_core_bg.wasm');
|
||||
}
|
||||
}
|
||||
]);
|
||||
Reference in New Issue
Block a user