Compare commits

..

39 Commits

Author SHA1 Message Date
Luke Street f984bc3fb2 Cleanup find_object_base & better error handling 2024-10-18 00:41:17 -06:00
cadmic 8823c2225e
Follow symlinks when looking for disc images (#78) 2024-10-18 00:37:54 -06:00
Luke Street fa5068fd6d Fix ELF relocation processing
Another bug caused by object removing ELF null section / symbol
from their iterators.
2024-10-18 00:24:34 -06:00
Luke Street 3ada073da1 Update dependencies 2024-10-18 00:19:06 -06:00
Luke Street 5c7560bcea Fix address analysis with negative `add` operands
Resolves #76
2024-10-14 09:40:46 -06:00
Luke Street 8d8d801b2f A couple process_elf fixes 2024-10-13 14:15:13 -06:00
Luke Street bee4570a4c Always check for extracted files in object resolution
Fixes an issue where extracted files would not be found after
removing the disc image from the orig dir.
2024-10-13 13:36:01 -06:00
Luke Street 18bd608fe8 Extract files from disc image to `object_base` by default
When `extract_objects` is enabled, objects will be extracted once
from a disc image into `object_base`, and then used directly from
`object_base` going forward.

This allows users to delete the disc image from their `orig` dir
once the initial build completes.
2024-10-13 00:53:45 -06:00
Luke Street 1a9736f8d9 ci: Add Rust workspace cache 2024-10-12 23:32:51 -06:00
Luke Street 4fe2608e07 Make `selfile` relative to `object_base` 2024-10-12 23:31:41 -06:00
Luke Street 601c8e1a5e ci: Setup python venv for cargo-zigbuild 2024-10-10 22:37:55 -06:00
Luke Street 2e524e6806 Use typed-path in place of std Path/PathBuf
This allows handling path conversions in a more structured way,
as well as avoiding needless UTF-8 checks. All argument inputs
use `Utf8NativePathBuf`, while all config entries use
`Utf8UnixPathBuf`, ensuring that we deserialize/serialize using
forward slashes. We can omit `.display()` and lossy UTF-8
conversions since all paths are known valid UTF-8.
2024-10-04 23:38:15 -06:00
Luke Street 64d0491256 Remove unused metroidbuildinfo command 2024-10-04 21:08:34 -06:00
Luke Street 4611a4b501 Vendor nintendo-lz crate to fix issues & avoid old deps 2024-10-04 21:02:04 -06:00
Luke Street b184fee73f Migrate SectionIndex/SymbolIndex to u32
This halves the size of structs like SectionAddress.
2024-10-04 20:40:50 -06:00
Luke Street 1f4b452bd5 Fix disc VFS layout & update nod 2024-10-04 20:33:46 -06:00
Luke Street f346239b81 Update README.md 2024-10-04 19:17:46 -06:00
Luke Street ef7e0db095 VFS fixes and improvements, update README.md
`vfs ls`: Now displays size, detected file format, and decompressed
size (if applicable). `-r`/`--recursive` lists files recursively.
`-s`/`--short` prints only filenames.

`vfs cp`: Copies files recursively when the source is a directory.
`--no-decompress` disables automatic decompression for Yay0/Yaz0.
`-q` disables printing copied files.

`rarc` and `u8` commands are now thin wrappers over `vfs ls` and
`vfs cp`. For example, `rarc list file.arc` is now equivalent to
`vfs ls file.arc:`. `rarc extract file.arc -o out` is equivalent
to `vfs cp file.arc: out`.
2024-10-04 18:15:24 -06:00
Luke Street 281b0f7104 Decode extab entries as comment in assembly output 2024-10-03 22:33:21 -06:00
Luke Street 71701b5667 Update all dependencies 2024-10-03 22:24:54 -06:00
Luke Street f91c2a1474 Load objects from disc image & `vfs` module
Revamps support for container paths and centralizes logic into a VFS (virtual file system) module.
The new VFS architecture supports disc images and any layer of nesting.

For example, the following command works:
`dtk dol info 'Interactive Multi-Game Demo Disc - July 2002 (USA).rvz:files/zz_StarFox051702_e3.tgc:files/default.dol'`
This opens a TGC file inside an RVZ disc image, then reads `default.dol` in the FST.

Another example:
`dtk rel info 'Legend of Zelda, The - The Wind Waker (USA).rvz:files/RELS.arc:mmem/f_pc_profile_lst.rel'`
This opens a RARC archive inside an RVZ disc image, loads the Yaz0-compressed REL and
decompresses it on the fly.

This all operates in memory with minimal overhead, with no need to extract temporary files.

Supported container formats:
- Disc images (ISO/GCM, WIA/RVZ, WBFS, CISO, NFS, GCZ, TGC)
- RARC/SZS and U8 (.arc)

Supported compression formats:
- Yaz0 (SZS)
- Yay0 (SZP)
- NLZSS (.lz)

Additionally, projects can utilize a new configuration key `object_base`:
```
object: orig/GZLE01/sys/main.dol
modules:
- object: orig/GZLE01/files/RELS.arc:rels/mmem/f_pc_profile_lst.rel
```
becomes
```
object_base: orig/GZLE01
object: sys/main.dol
modules:
- object: files/RELS.arc:mmem/f_pc_profile_lst.rel
```
When loading the objects, decomp-toolkit will automatically check the `object_base`
directory for any disc images. (They can be named anything, but must be in the folder
root) If one is found, all objects will be fetched from the disc image itself, rather
than having to extract the files manually.

While still a work in progress, two new `vfs` commands were added: `vfs ls` and `vfs cp`.
These commands are very barebones currently, but allow listing directory contents and
extracting files from decomp-toolkit's vfs representation:
```
❯ dtk vfs ls disc.rvz:
files
sys

❯ dtk vfs ls disc.rvz:sys
boot.bin
bi2.bin
apploader.bin
fst.bin
main.dol

❯ dtk vfs cp disc.rvz:sys/main.dol .
```
2024-10-03 21:50:35 -06:00
Amber Brault 26f52f65b7
Automatically check for invalid extab relocations (#75)
* Begin work on extab reloc analysis code

* Refactoring

* Make format happy

* Address feedback + improvements
2024-10-03 01:13:23 -06:00
Luke Street c106123877 Use mimalloc when targeting musl
Also removes the armv7 linux build.
If you were using this, let me know!

Updates all dependencies
2024-09-29 12:20:28 -06:00
Luke Street dfda3d5ea3 Fix todo! in FileIterator::next_path 2024-09-29 12:02:55 -06:00
Luke Street 68f4552e44 Better support for SMG/TP maps
These maps are post-processed and have
various issues. This includes some hacks
to work around those issues as much as
possible.
2024-09-29 12:02:26 -06:00
Luke Street ac45676770 Fixes for updated object crate
object stopped including the ELF null
symbol and null section in the respective
iterators. We relied on this behavior for
building certain vectors in order of
symbol index. Adjust this logic to
restore the correct behavior.
2024-09-29 12:00:44 -06:00
Luke Street e430cb56f5 Remove features from CI 2024-09-09 22:05:33 -06:00
Luke Street 0719c73ef8 Resolve clippy issue 2024-09-09 20:39:54 -06:00
Luke Street cfcd146dfa Add map config for generating symbols/splits
Useful for extracting information from
map files that aren't fully well-formed,
such as ones from existing decompilation
projects.
2024-09-09 20:38:25 -06:00
Luke Street d4f695ffc7 dol diff: Loosen @ symbol match criteria 2024-09-09 20:36:46 -06:00
Luke Street 8b793b5616 Update CI workflow & all dependencies 2024-09-09 20:36:18 -06:00
Luke Street 9dfdbb9301 Fix v1-2 REL alignment regression
Alignment after section data and
before relocations / import table
is exclusive to REL v3.
2024-09-05 00:26:14 -06:00
Luke Street c403931f0f Update nod for TGC disc support 2024-09-04 20:09:56 -06:00
Luke Street d9817f63d5 Fix .note.split warnings for older mwld versions
Prior to mwld GC 3.0a3, the linker doesn't support ELF .note sections
properly. With GC 2.7, it crashes if the section type is SHT_NOTE.
Use the same section type as .mwcats.* so the linker ignores it.
2024-09-04 19:56:22 -06:00
Chippy a112eb1829
DWARF: Add PureVirtual/Virtual Block2 attributes (#70) 2024-08-26 17:41:25 -07:00
Luke Street b6a29fa910 Add split `order` attribute for manual reordering
Example in splits.txt:
```
file1.cpp: order:0
  ...

file2.cpp: order:1
  ...

file3.cpp: order:2
  ...
```

This ensures that file2.cpp is always
anchored in between 1 and 3 when resolving
the final link order.
2024-08-11 20:38:11 -06:00
Wesley Moret da6a514fac
RSO: `make` command (#67)
Allow to create rso file from relocatable elf

No sel file support yet
2024-08-06 21:15:03 -06:00
riidefi cfeacd2c3a
elf2dol: Support section name denylist (#64) 2024-07-17 20:02:34 -06:00
First Last c3c7c2b062
Properly locate ProDG .bss sections (partial addressing of #62) (#63)
* Locate ProDG .bss sections (partial addressing of #62)

* Support both correct and incorrect memset calls
2024-07-16 23:14:46 -06:00
61 changed files with 5121 additions and 2419 deletions

View File

@ -1,16 +1,17 @@
name: Build
on:
pull_request:
push:
paths-ignore:
- '*.md'
- 'LICENSE*'
pull_request:
workflow_dispatch:
env:
BUILD_PROFILE: release-lto
CARGO_BIN_NAME: dtk
CARGO_TARGET_DIR: target
CARGO_INCREMENTAL: 0
jobs:
check:
@ -25,6 +26,8 @@ jobs:
uses: dtolnay/rust-toolchain@stable
with:
components: clippy
- name: Cache Rust workspace
uses: Swatinem/rust-cache@v2
- name: Cargo check
run: cargo check --all-features --all-targets
- name: Cargo clippy
@ -74,11 +77,15 @@ jobs:
uses: actions/checkout@v4
- name: Setup Rust toolchain
uses: dtolnay/rust-toolchain@stable
- name: Cache Rust workspace
uses: Swatinem/rust-cache@v2
- name: Cargo test
run: cargo test --release --all-features
build:
name: Build
name: Build dtk
env:
CARGO_BIN_NAME: dtk
strategy:
matrix:
include:
@ -94,10 +101,10 @@ jobs:
target: aarch64-unknown-linux-musl
name: linux-aarch64
build: zigbuild
- platform: ubuntu-latest
target: armv7-unknown-linux-musleabi
name: linux-armv7l
build: zigbuild
- platform: windows-latest
target: i686-pc-windows-msvc
name: windows-x86
build: build
- platform: windows-latest
target: x86_64-pc-windows-msvc
name: windows-x86_64
@ -119,19 +126,6 @@ jobs:
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Check git tag against Cargo version
if: startsWith(github.ref, 'refs/tags/')
shell: bash
run: |
set -eou
version=$(grep '^version' Cargo.toml | awk -F' = ' '{print $2}' | tr -d '"')
version="v$version"
tag='${{github.ref}}'
tag="${tag#refs/tags/}"
if [ "$tag" != "$version" ]; then
echo "::error::Git tag doesn't match the Cargo version! ($tag != $version)"
exit 1
fi
- name: Install dependencies
if: matrix.packages != ''
run: |
@ -139,20 +133,28 @@ jobs:
sudo apt-get -y install ${{ matrix.packages }}
- name: Install cargo-zigbuild
if: matrix.build == 'zigbuild'
run: pip install ziglang==0.12.0 cargo-zigbuild==0.18.4
run: |
python3 -m venv .venv
. .venv/bin/activate
echo PATH=$PATH >> $GITHUB_ENV
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: Cache Rust workspace
uses: Swatinem/rust-cache@v2
with:
key: ${{ matrix.target }}
- name: Cargo build
run: cargo ${{ matrix.build }} --profile ${{ env.BUILD_PROFILE }} --all-features --target ${{ matrix.target }} --bin ${{ env.CARGO_BIN_NAME }}
run: >
cargo ${{ matrix.build }} --profile ${{ env.BUILD_PROFILE }} --target ${{ matrix.target }}
--bin ${{ env.CARGO_BIN_NAME }}
- name: Upload artifacts
uses: actions/upload-artifact@v4
with:
name: ${{ matrix.name }}
name: ${{ env.CARGO_BIN_NAME }}-${{ matrix.name }}
path: |
${{ env.CARGO_TARGET_DIR }}/${{ env.BUILD_PROFILE }}/${{ env.CARGO_BIN_NAME }}
${{ env.CARGO_TARGET_DIR }}/${{ env.BUILD_PROFILE }}/${{ env.CARGO_BIN_NAME }}.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
@ -162,7 +164,23 @@ jobs:
if: startsWith(github.ref, 'refs/tags/')
runs-on: ubuntu-latest
needs: [ build ]
permissions:
contents: write
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Check git tag against Cargo version
shell: bash
run: |
set -eou pipefail
tag='${{github.ref}}'
tag="${tag#refs/tags/}"
version=$(grep '^version' Cargo.toml | 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
- name: Download artifacts
uses: actions/download-artifact@v4
with:
@ -170,12 +188,28 @@ jobs:
- name: Rename artifacts
working-directory: artifacts
run: |
set -euo pipefail
mkdir ../out
for i in */*/$BUILD_PROFILE/$CARGO_BIN_NAME*; do
mv "$i" "../out/$(sed -E "s/([^/]+)\/[^/]+\/$BUILD_PROFILE\/($CARGO_BIN_NAME)/\2-\1/" <<< "$i")"
for dir in */; do
for file in "$dir"*; do
base=$(basename "$file")
name="${base%.*}"
ext="${base##*.}"
if [ "$ext" = "$base" ]; then
ext=""
else
ext=".$ext"
fi
arch="${dir%/}" # remove trailing slash
arch="${arch##"$name-"}" # remove bin name
dst="../out/${name}-${arch}${ext}"
mv "$file" "$dst"
done
done
ls -R ../out
- name: Release
uses: softprops/action-gh-release@v2
with:
files: out/*
draft: true
generate_release_notes: true

983
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@ -3,13 +3,13 @@ name = "decomp-toolkit"
description = "Yet another GameCube/Wii decompilation toolkit."
authors = ["Luke Street <luke@street.dev>"]
license = "MIT OR Apache-2.0"
version = "0.9.3"
version = "1.1.4"
edition = "2021"
publish = false
repository = "https://github.com/encounter/decomp-toolkit"
readme = "README.md"
categories = ["command-line-utilities"]
rust-version = "1.73.0"
rust-version = "1.81"
[[bin]]
name = "dtk"
@ -20,56 +20,63 @@ panic = "abort"
[profile.release-lto]
inherits = "release"
lto = "thin"
lto = "fat"
strip = "debuginfo"
codegen-units = 1
[dependencies]
anyhow = { version = "1.0.82", features = ["backtrace"] }
anyhow = { version = "1.0", features = ["backtrace"] }
ar = { git = "https://github.com/bjorn3/rust-ar.git", branch = "write_symbol_table" }
argp = "0.3.0"
base16ct = "0.2.0"
base64 = "0.22.1"
crossterm = "0.27.0"
cwdemangle = "1.0.0"
enable-ansi-support = "0.2.1"
filetime = "0.2.23"
fixedbitset = "0.5.7"
flagset = { version = "0.4.5", features = ["serde"] }
glob = "0.3.1"
hex = "0.4.3"
indent = "0.1.1"
indexmap = "2.2.6"
itertools = "0.12.1"
log = "0.4.21"
memchr = "2.7.2"
memmap2 = "0.9.4"
multimap = "0.10.0"
nintendo-lz = "0.1.3"
nodtool = { git = "https://github.com/encounter/nod-rs", rev = "03b83484cb17f94408fa0ef8e50d94951464d1b2" }
argp = "0.3"
base16ct = "0.2"
base64 = "0.22"
byteorder = "1.5"
typed-path = "0.9"
crossterm = "0.28"
cwdemangle = "1.0"
cwextab = "1.0"
dyn-clone = "1.0"
enable-ansi-support = "0.2"
filetime = "0.2"
fixedbitset = "0.5"
flagset = { version = "0.4", features = ["serde"] }
glob = "0.3"
hex = "0.4"
indent = "0.1"
indexmap = "2.6"
itertools = "0.13"
log = "0.4"
memchr = "2.7"
memmap2 = "0.9"
multimap = "0.10"
nodtool = "1.4"
#nodtool = { path = "../nod-rs/nodtool" }
num_enum = "0.7.2"
objdiff-core = { git = "https://github.com/encounter/objdiff", rev = "a5a6a3928e392d5af5d92826e73b77e074b8788c", features = ["ppc"] }
num_enum = "0.7"
objdiff-core = { version = "2.2", features = ["ppc"] }
#objdiff-core = { path = "../objdiff/objdiff-core", features = ["ppc"] }
object = { version = "0.35.0", features = ["read_core", "std", "elf", "write_std"], default-features = false }
once_cell = "1.19.0"
orthrus-ncompress = "0.2.1"
owo-colors = { version = "4.0.0", features = ["supports-colors"] }
path-slash = "0.2.1"
petgraph = { version = "0.6.4", default-features = false }
ppc750cl = { git = "https://github.com/encounter/ppc750cl", rev = "6cbd7d888c7082c2c860f66cbb9848d633f753ed" }
rayon = "1.10.0"
regex = "1.10.4"
rustc-hash = "1.1.0"
sanitise-file-name = "1.0.0"
serde = "1.0.199"
serde_json = "1.0.116"
serde_repr = "0.1.19"
serde_yaml = "0.9.34"
sha-1 = "0.10.1"
supports-color = "3.0.0"
syntect = { version = "5.2.0", features = ["parsing", "regex-fancy", "dump-load"], default-features = false }
tracing = "0.1.40"
tracing-attributes = "0.1.27"
tracing-subscriber = { version = "0.3.18", features = ["env-filter"] }
xxhash-rust = { version = "0.8.10", features = ["xxh3"] }
zerocopy = { version = "0.7.34", features = ["derive"] }
object = { version = "0.36", features = ["read_core", "std", "elf", "write_std"], default-features = false }
once_cell = "1.20"
orthrus-ncompress = "0.2"
owo-colors = { version = "4.1", features = ["supports-colors"] }
petgraph = { version = "0.6", default-features = false }
ppc750cl = "0.3"
rayon = "1.10"
regex = "1.11"
rustc-hash = "2.0"
sanitise-file-name = "1.0"
serde = "1.0"
serde_json = "1.0"
serde_repr = "0.1"
serde_yaml = "0.9"
sha-1 = "0.10"
size = "0.4"
supports-color = "3.0"
syntect = { version = "5.2", features = ["parsing", "regex-fancy", "dump-load"], default-features = false }
tracing = "0.1"
tracing-attributes = "0.1"
tracing-subscriber = { version = "0.3", features = ["env-filter"] }
xxhash-rust = { version = "0.8", features = ["xxh3"] }
zerocopy = { version = "0.8", features = ["derive"] }
[target.'cfg(target_env = "musl")'.dependencies]
mimalloc = "0.1"

127
README.md
View File

@ -39,12 +39,15 @@ project structure and build system that uses decomp-toolkit under the hood.
- [rel info](#rel-info)
- [rel merge](#rel-merge)
- [rso info](#rso-info)
- [rso make](#rso-make)
- [shasum](#shasum)
- [nlzss decompress](#nlzss-decompress)
- [rarc list](#rarc-list)
- [rarc extract](#rarc-extract)
- [u8 list](#u8-list)
- [u8 extract](#u8-extract)
- [vfs ls](#vfs-ls)
- [vfs cp](#vfs-cp)
- [yay0 decompress](#yay0-decompress)
- [yay0 compress](#yay0-compress)
- [yaz0 decompress](#yaz0-decompress)
@ -88,8 +91,6 @@ binary that is byte-for-byte identical to the original, then we know that the de
decomp-toolkit provides tooling for analyzing and splitting the original binary into relocatable objects, as well
as generating the linker script and other files needed to link the decompiled code.
## Analyzer features
**Function boundary analysis**
@ -125,11 +126,6 @@ If desired, optionally writes GNU assembler-compatible files alongside the objec
**Linker script generation**
Generates `ldscript.lcf` for `mwldeppc.exe`.
**Future work**
- Support RSO files
- Add more signatures
## Commands
### ar create
@ -180,6 +176,8 @@ and its `nodtool` command line tool._
Displays information about disc images.
To list the contents of a disc image, use [vfs ls](#vfs-ls).
Supported disc image formats:
- ISO (GCM)
@ -188,6 +186,7 @@ Supported disc image formats:
- CISO (+ NKit 2 lossless)
- NFS (Wii U VC)
- GCZ
- TGC
```shell
$ dtk disc info /path/to/game.iso
@ -199,6 +198,9 @@ Extracts the contents of disc images to a directory.
See [disc info](#disc-info) for supported formats.
> [!NOTE]
> [vfs cp](#vfs-cp) is more flexible and supports disc images.
```shell
$ dtk disc extract /path/to/game.iso [outdir]
```
@ -233,21 +235,23 @@ $ dtk disc verify /path/to/game.iso
Analyzes a DOL file and outputs information section and symbol information.
See [vfs ls](#vfs-ls) for information on the VFS abstraction.
```shell
$ dtk dol info input.dol
# or, directly from a disc image
$ dtk dol info 'disc.rvz:sys/main.dol'
```
### dol split
> [!NOTE]
> This command is a work-in-progress.
> [!IMPORTANT]
> **This command is intended to be used as part of a decompilation project's build system.**
> For an example project structure and for documentation on the configuration, see
> [dtk-template](https://github.com/encounter/dtk-template).
Analyzes and splits a DOL file into relocatable objects based on user configuration.
**This command is intended to be used as part of a decompilation project's build system.**
For an example project structure and for documentation on the configuration, see
[dtk-template](https://github.com/encounter/dtk-template).
```shell
$ dtk dol split config.yml target
```
@ -321,6 +325,8 @@ Creates a DOL file from the provided ELF file.
```shell
$ dtk elf2dol input.elf output.dol
# or, to ignore certain sections
$ dtk elf2dol input.elf output.dol --ignore debug_section1 --ignore debug_section2
```
### map
@ -344,8 +350,12 @@ $ dtk map symbol Game.MAP 'Function__5ClassFv'
Prints information about a REL file.
See [vfs ls](#vfs-ls) for information on the VFS abstraction.
```shell
$ dtk rel info input.rel
# or, directly from a disc image
$ dtk rel info 'disc.rvz:files/RELS.arc:amem/d_a_tag_so.rel'
```
### rel merge
@ -368,6 +378,22 @@ Prints information about an RSO file.
$ dtk rso info input.rso
```
### rso make
> [!WARNING]
> This command does not yet support creating SEL files.
Creates an RSO file from a relocatable ELF file.
Options:
- `-o`, `--output <File>`: Output RSO file.
- `-m`, `--module-name <Name>`: Module name (or path). Default: input name
- `-e`, `--export <File>`: File containing exported symbol names. (Newline separated)
```shell
$ dtk rso make input.elf -o input.rso
```
### shasum
Calculate and verify SHA-1 hashes.
@ -392,6 +418,10 @@ $ dtk nlzss decompress rels/*.lz -o rels
### rarc list
> [!NOTE]
> [vfs ls](#vfs-ls) is more flexible and supports RARC archives.
> This command is now equivalent to `dtk vfs ls -r input.arc:`
Lists the contents of an RARC (older .arc) archive.
```shell
@ -400,6 +430,10 @@ $ dtk rarc list input.arc
### rarc extract
> [!NOTE]
> [vfs cp](#vfs-cp) is more flexible and supports RARC archives.
> This command is now equivalent to `dtk vfs cp input.arc: output_dir`
Extracts the contents of an RARC (older .arc) archive.
```shell
@ -408,6 +442,10 @@ $ dtk rarc extract input.arc -o output_dir
### u8 list
> [!NOTE]
> [vfs ls](#vfs-ls) is more flexible and supports U8 archives.
> This command is now equivalent to `dtk vfs ls -r input.arc:`
Extracts the contents of a U8 (newer .arc) archive.
```shell
@ -416,12 +454,75 @@ $ dtk u8 list input.arc
### u8 extract
> [!NOTE]
> [vfs cp](#vfs-cp) is more flexible and supports U8 archives.
> This command is now equivalent to `dtk vfs cp input.arc: output_dir`
Extracts the contents of a U8 (newer .arc) archive.
```shell
$ dtk u8 extract input.arc -o output_dir
```
### vfs ls
decomp-toolkit has a powerful virtual filesystem (VFS) abstraction that allows you to work with a
variety of containers. All operations happen in memory with minimal overhead and no temporary files.
Supported containers:
- Disc images (see [disc info](#disc-info) for supported formats)
- RARC archives (older .arc)
- U8 archives (newer .arc)
Supported compression formats are handled transparently:
- Yay0 (SZP) / Yaz0 (SZS)
- NLZSS (.lz) (Use `:nlzss` in the path)
`vfs ls` lists the contents of a container or directory.
Options:
- `-r`, `--recursive`: Recursively list contents.
- `-s`, `--short`: Only list file names.
Examples:
```shell
# List the contents of the `amem` directory inside `RELS.arc` in a disc image
$ dtk vfs ls 'disc.rvz:files/RELS.arc:amem'
# List the contents of `RELS.arc` recursively
$ dtk vfs ls -r 'disc.rvz:files/RELS.arc:'
# All commands that accept a file path can also accept a VFS path
$ dtk rel info 'disc.rvz:files/RELS.arc:amem/d_a_tag_so.rel'
# Example disc image within a disc image
$ dtk dol info 'disc.rvz:files/zz_demo.tgc:sys/main.dol'
````
### vfs cp
See [vfs ls](#vfs-ls) for information on the VFS abstraction.
`vfs cp` copies files and directories recursively to the host filesystem.
Options:
- `--no-decompress`: Do not decompress files when copying.
- `-q`, `--quiet`: Suppresses all output except errors.
Examples:
```shell
# Extract a file from a nested path in a disc image to the current directory
$ dtk vfs cp 'disc.rvz:files/RELS.arc:amem/d_a_tag_so.rel' .
# Directories are copied recursively, making it easy to extract entire archives
$ dtk vfs cp 'disc.rvz:files/RELS.arc:' rels
# Or, to disable automatic decompression
$ dtk vfs cp --no-decompress 'disc.rvz:files/RELS.arc:' rels
```
### yay0 decompress
Decompresses Yay0-compressed files.

182
deny.toml
View File

@ -9,6 +9,11 @@
# The values provided in this template are the default values that will be used
# when any section or field is not specified in your own configuration
# Root options
# The graph table configures how the dependency graph is constructed and thus
# which crates the checks are performed against
[graph]
# If 1 or more target triples (and optionally, target_features) are specified,
# only the specified targets will be checked when running `cargo deny check`.
# This means, if a particular package is only ever used as a target specific
@ -20,53 +25,67 @@
targets = [
# The triple can be any string, but only the target triples built in to
# rustc (as of 1.40) can be checked against actual config expressions
#{ triple = "x86_64-unknown-linux-musl" },
#"x86_64-unknown-linux-musl",
# You can also specify which target_features you promise are enabled for a
# particular target. target_features are currently not validated against
# the actual valid features supported by the target architecture.
#{ triple = "wasm32-unknown-unknown", features = ["atomics"] },
]
# When creating the dependency graph used as the source of truth when checks are
# executed, this field can be used to prune crates from the graph, removing them
# from the view of cargo-deny. This is an extremely heavy hammer, as if a crate
# is pruned from the graph, all of its dependencies will also be pruned unless
# they are connected to another crate in the graph that hasn't been pruned,
# so it should be used with care. The identifiers are [Package ID Specifications]
# (https://doc.rust-lang.org/cargo/reference/pkgid-spec.html)
#exclude = []
# If true, metadata will be collected with `--all-features`. Note that this can't
# be toggled off if true, if you want to conditionally enable `--all-features` it
# is recommended to pass `--all-features` on the cmd line instead
all-features = false
# If true, metadata will be collected with `--no-default-features`. The same
# caveat with `all-features` applies
no-default-features = false
# If set, these feature will be enabled when collecting metadata. If `--features`
# is specified on the cmd line they will take precedence over this option.
#features = []
# The output table provides options for how/if diagnostics are outputted
[output]
# When outputting inclusion graphs in diagnostics that include features, this
# option can be used to specify the depth at which feature edges will be added.
# This option is included since the graphs can be quite large and the addition
# of features from the crate(s) to all of the graph roots can be far too verbose.
# This option can be overridden via `--feature-depth` on the cmd line
feature-depth = 1
# This section is considered when running `cargo deny check advisories`
# More documentation for the advisories section can be found here:
# https://embarkstudios.github.io/cargo-deny/checks/advisories/cfg.html
[advisories]
# The path where the advisory database is cloned/fetched into
db-path = "~/.cargo/advisory-db"
# The path where the advisory databases are cloned/fetched into
#db-path = "$CARGO_HOME/advisory-dbs"
# The url(s) of the advisory databases to use
db-urls = ["https://github.com/rustsec/advisory-db"]
# The lint level for security vulnerabilities
vulnerability = "deny"
# The lint level for unmaintained crates
unmaintained = "warn"
# The lint level for crates that have been yanked from their source registry
yanked = "warn"
# The lint level for crates with security notices. Note that as of
# 2019-12-17 there are no security notice advisories in
# https://github.com/rustsec/advisory-db
notice = "warn"
#db-urls = ["https://github.com/rustsec/advisory-db"]
# A list of advisory IDs to ignore. Note that ignored advisories will still
# output a note when they are encountered.
ignore = [
#"RUSTSEC-0000-0000",
#{ id = "RUSTSEC-0000-0000", reason = "you can specify a reason the advisory is ignored" },
#"a-crate-that-is-yanked@0.1.1", # you can also ignore yanked crate versions if you wish
#{ crate = "a-crate-that-is-yanked@0.1.1", reason = "you can specify why you are ignoring the yanked crate" },
]
# Threshold for security vulnerabilities, any vulnerability with a CVSS score
# lower than the range specified will be ignored. Note that ignored advisories
# will still output a note when they are encountered.
# * None - CVSS Score 0.0
# * Low - CVSS Score 0.1 - 3.9
# * Medium - CVSS Score 4.0 - 6.9
# * High - CVSS Score 7.0 - 8.9
# * Critical - CVSS Score 9.0 - 10.0
#severity-threshold =
# If this is true, then cargo deny will use the git executable to fetch advisory database.
# If this is false, then it uses a built-in git library.
# Setting this to true can be helpful if you have special authentication requirements that cargo-deny does not support.
# See Git Authentication for more information about setting up git authentication.
#git-fetch-with-cli = true
# This section is considered when running `cargo deny check licenses`
# More documentation for the licenses section can be found here:
# https://embarkstudios.github.io/cargo-deny/checks/licenses/cfg.html
[licenses]
# The lint level for crates which do not have a detectable license
unlicensed = "deny"
# List of explictly allowed licenses
# List of explicitly allowed licenses
# See https://spdx.org/licenses/ for list of possible licenses
# [possible values: any SPDX 3.11 short identifier (+ optional exception)].
allow = [
@ -77,28 +96,10 @@ allow = [
"BSL-1.0",
"ISC",
"MIT",
"MPL-2.0",
"Unicode-DFS-2016",
"Zlib",
]
# List of explictly disallowed licenses
# See https://spdx.org/licenses/ for list of possible licenses
# [possible values: any SPDX 3.11 short identifier (+ optional exception)].
deny = [
#"Nokia",
]
# Lint level for licenses considered copyleft
copyleft = "warn"
# Blanket approval or denial for OSI-approved or FSF Free/Libre licenses
# * both - The license will be approved if it is both OSI-approved *AND* FSF
# * either - The license will be approved if it is either OSI-approved *OR* FSF
# * osi-only - The license will be approved if is OSI-approved *AND NOT* FSF
# * fsf-only - The license will be approved if is FSF *AND NOT* OSI-approved
# * neither - This predicate is ignored and the default lint level is used
allow-osi-fsf-free = "neither"
# Lint level used when no other predicates are matched
# 1. License isn't in the allow or deny lists
# 2. License isn't copyleft
# 3. License isn't OSI/FSF, or allow-osi-fsf-free = "neither"
default = "deny"
# The confidence threshold for detecting a license from license text.
# The higher the value, the more closely the license text must be to the
# canonical license text of a valid SPDX license file.
@ -109,32 +110,32 @@ confidence-threshold = 0.8
exceptions = [
# Each entry is the crate and version constraint, and its specific allow
# list
#{ allow = ["Zlib"], name = "adler32", version = "*" },
#{ allow = ["Zlib"], crate = "adler32" },
]
# Some crates don't have (easily) machine readable licensing information,
# adding a clarification entry for it allows you to manually specify the
# licensing information
[[licenses.clarify]]
# The name of the crate the clarification applies to
name = "encoding_rs"
# The optional version constraint for the crate
#version = "*"
#[[licenses.clarify]]
# The package spec the clarification applies to
#crate = "ring"
# The SPDX expression for the license requirements of the crate
expression = "(Apache-2.0 OR MIT) AND BSD-3-Clause"
#expression = "MIT AND ISC AND OpenSSL"
# One or more files in the crate's source used as the "source of truth" for
# the license expression. If the contents match, the clarification will be used
# when running the license check, otherwise the clarification will be ignored
# and the crate will be checked normally, which may produce warnings or errors
# depending on the rest of your configuration
license-files = [
# Each entry is a crate relative path, and the (opaque) hash of its contents
{ path = "COPYRIGHT", hash = 0x39f8ad31 }
]
#license-files = [
# Each entry is a crate relative path, and the (opaque) hash of its contents
#{ path = "LICENSE", hash = 0xbd0eed23 }
#]
[licenses.private]
# If true, ignores workspace crates that aren't published, or are only
# published to private registries
# published to private registries.
# To see how to mark a crate as unpublished (to the official registry),
# visit https://doc.rust-lang.org/cargo/reference/manifest.html#the-publish-field.
ignore = false
# One or more private registries that you might publish crates to, if a crate
# is only published to private registries, and ignore is true, the crate will
@ -148,7 +149,7 @@ registries = [
# https://embarkstudios.github.io/cargo-deny/checks/bans/cfg.html
[bans]
# Lint level for when multiple versions of the same crate are detected
multiple-versions = "warn"
multiple-versions = "allow"
# Lint level for when a crate version requirement is `*`
wildcards = "allow"
# The graph highlighting used when creating dotgraphs for crates
@ -157,30 +158,63 @@ wildcards = "allow"
# * simplest-path - The path to the version with the fewest edges is highlighted
# * all - Both lowest-version and simplest-path are used
highlight = "all"
# The default lint level for `default` features for crates that are members of
# the workspace that is being checked. This can be overridden by allowing/denying
# `default` on a crate-by-crate basis if desired.
workspace-default-features = "allow"
# The default lint level for `default` features for external crates that are not
# members of the workspace. This can be overridden by allowing/denying `default`
# on a crate-by-crate basis if desired.
external-default-features = "allow"
# List of crates that are allowed. Use with care!
allow = [
#{ name = "ansi_term", version = "=0.11.0" },
#"ansi_term@0.11.0",
#{ crate = "ansi_term@0.11.0", reason = "you can specify a reason it is allowed" },
]
# List of crates to deny
deny = [
# Each entry the name of a crate and a version range. If version is
# not specified, all versions will be matched.
#{ name = "ansi_term", version = "=0.11.0" },
#
#"ansi_term@0.11.0",
#{ crate = "ansi_term@0.11.0", reason = "you can specify a reason it is banned" },
# Wrapper crates can optionally be specified to allow the crate when it
# is a direct dependency of the otherwise banned crate
#{ name = "ansi_term", version = "=0.11.0", wrappers = [] },
#{ crate = "ansi_term@0.11.0", wrappers = ["this-crate-directly-depends-on-ansi_term"] },
]
# List of features to allow/deny
# Each entry the name of a crate and a version range. If version is
# not specified, all versions will be matched.
#[[bans.features]]
#crate = "reqwest"
# Features to not allow
#deny = ["json"]
# Features to allow
#allow = [
# "rustls",
# "__rustls",
# "__tls",
# "hyper-rustls",
# "rustls",
# "rustls-pemfile",
# "rustls-tls-webpki-roots",
# "tokio-rustls",
# "webpki-roots",
#]
# If true, the allowed features must exactly match the enabled feature set. If
# this is set there is no point setting `deny`
#exact = true
# Certain crates/versions that will be skipped when doing duplicate detection.
skip = [
#{ name = "ansi_term", version = "=0.11.0" },
#"ansi_term@0.11.0",
#{ crate = "ansi_term@0.11.0", reason = "you can specify a reason why it can't be updated/removed" },
]
# Similarly to `skip` allows you to skip certain crates during duplicate
# detection. Unlike skip, it also includes the entire tree of transitive
# dependencies starting at the specified crate, up to a certain depth, which is
# by default infinite
# by default infinite.
skip-tree = [
#{ name = "ansi_term", version = "=0.11.0", depth = 20 },
#"ansi_term@0.11.0", # will be skipped along with _all_ of its direct and transitive dependencies
#{ crate = "ansi_term@0.11.0", depth = 20 },
]
# This section is considered when running `cargo deny check sources`.
@ -200,9 +234,9 @@ allow-registry = ["https://github.com/rust-lang/crates.io-index"]
allow-git = []
[sources.allow-org]
# 1 or more github.com organizations to allow git sources for
#github = [""]
# 1 or more gitlab.com organizations to allow git sources for
#gitlab = [""]
# 1 or more bitbucket.org organizations to allow git sources for
#bitbucket = [""]
# github.com organizations to allow git sources for
github = ["bjorn3"]
# gitlab.com organizations to allow git sources for
gitlab = []
# bitbucket.org organizations to allow git sources for
bitbucket = []

View File

@ -16,12 +16,15 @@ use crate::{
vm::{BranchTarget, GprValue, StepResult, VM},
RelocationTarget,
},
obj::{ObjInfo, ObjSectionKind, ObjSymbol, ObjSymbolFlagSet, ObjSymbolFlags, ObjSymbolKind},
obj::{
ObjInfo, ObjSectionKind, ObjSymbol, ObjSymbolFlagSet, ObjSymbolFlags, ObjSymbolKind,
SectionIndex,
},
};
#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub struct SectionAddress {
pub section: usize,
pub section: SectionIndex,
pub address: u32,
}
@ -38,7 +41,7 @@ impl Display for SectionAddress {
}
impl SectionAddress {
pub fn new(section: usize, address: u32) -> Self { Self { section, address } }
pub fn new(section: SectionIndex, address: u32) -> Self { Self { section, address } }
pub fn offset(self, offset: i32) -> Self {
Self { section: self.section, address: self.address.wrapping_add_signed(offset) }
@ -53,6 +56,10 @@ impl SectionAddress {
}
pub fn is_aligned(self, align: u32) -> bool { self.address & (align - 1) == 0 }
pub fn wrapping_add(self, rhs: u32) -> Self {
Self { section: self.section, address: self.address.wrapping_add(rhs) }
}
}
impl Add<u32> for SectionAddress {
@ -116,7 +123,7 @@ pub struct AnalyzerState {
pub functions: BTreeMap<SectionAddress, FunctionInfo>,
pub jump_tables: BTreeMap<SectionAddress, u32>,
pub known_symbols: BTreeMap<SectionAddress, Vec<ObjSymbol>>,
pub known_sections: BTreeMap<usize, String>,
pub known_sections: BTreeMap<SectionIndex, String>,
}
impl AnalyzerState {
@ -597,19 +604,27 @@ pub fn locate_bss_memsets(obj: &mut ObjInfo) -> Result<Vec<(u32, u32)>> {
StepResult::Branch(branches) => {
for branch in branches {
if branch.link {
// ProDG bug? Registers are supposed to start at r3
// Some ProDG crt0.s versions use the wrong registers, some don't
if let (
GprValue::Constant(addr),
GprValue::Constant(value),
GprValue::Constant(size),
) = (vm.gpr_value(4), vm.gpr_value(5), vm.gpr_value(6))
{
) = {
if vm.gpr_value(4) == GprValue::Constant(0) {
(vm.gpr_value(3), vm.gpr_value(4), vm.gpr_value(5))
} else {
(vm.gpr_value(4), vm.gpr_value(5), vm.gpr_value(6))
}
} {
if value == 0 && size > 0 {
bss_sections.push((addr, size));
}
}
}
}
if bss_sections.len() >= 2 {
return Ok(ExecCbResult::End(()));
}
Ok(ExecCbResult::Continue)
}
}

View File

@ -18,7 +18,7 @@ struct VisitedAddresses {
impl VisitedAddresses {
pub fn new(obj: &ObjInfo) -> Self {
let mut inner = Vec::with_capacity(obj.sections.len());
let mut inner = Vec::with_capacity(obj.sections.len() as usize);
for (_, section) in obj.sections.iter() {
if section.kind == ObjSectionKind::Code {
let size = (section.size / 4) as usize;
@ -32,11 +32,13 @@ impl VisitedAddresses {
}
pub fn contains(&self, section_address: u32, address: SectionAddress) -> bool {
self.inner[address.section].contains(Self::bit_for(section_address, address.address))
self.inner[address.section as usize]
.contains(Self::bit_for(section_address, address.address))
}
pub fn insert(&mut self, section_address: u32, address: SectionAddress) {
self.inner[address.section].insert(Self::bit_for(section_address, address.address));
self.inner[address.section as usize]
.insert(Self::bit_for(section_address, address.address));
}
#[inline]

View File

@ -6,7 +6,9 @@ use ppc750cl::Ins;
use crate::{
analysis::cfa::SectionAddress,
array_ref,
obj::{ObjInfo, ObjKind, ObjRelocKind, ObjSection, ObjSectionKind, ObjSymbolKind},
obj::{
ObjInfo, ObjKind, ObjRelocKind, ObjSection, ObjSectionKind, ObjSymbolKind, SectionIndex,
},
};
pub mod cfa;
@ -36,11 +38,9 @@ fn read_unresolved_relocation_address(
address: u32,
reloc_kind: Option<ObjRelocKind>,
) -> Result<Option<RelocationTarget>> {
if let Some(reloc) = obj
.unresolved_relocations
.iter()
.find(|reloc| reloc.section as usize == section.elf_index && reloc.address == address)
{
if let Some(reloc) = obj.unresolved_relocations.iter().find(|reloc| {
reloc.section as SectionIndex == section.elf_index && reloc.address == address
}) {
if reloc.module_id != obj.module_id {
return Ok(Some(RelocationTarget::External));
}
@ -48,7 +48,7 @@ fn read_unresolved_relocation_address(
ensure!(reloc.kind == reloc_kind);
}
let (target_section_index, target_section) =
obj.sections.get_elf_index(reloc.target_section as usize).ok_or_else(|| {
obj.sections.get_elf_index(reloc.target_section as SectionIndex).ok_or_else(|| {
anyhow!(
"Failed to find target section {} for unresolved relocation",
reloc.target_section

View File

@ -1,7 +1,7 @@
use anyhow::Result;
use crate::{
obj::{ObjDataKind, ObjInfo, ObjSectionKind, ObjSymbolKind},
obj::{ObjDataKind, ObjInfo, ObjSectionKind, ObjSymbolKind, SymbolIndex},
util::split::is_linker_generated_label,
};
@ -64,7 +64,7 @@ pub fn detect_objects(obj: &mut ObjInfo) -> Result<()> {
}
pub fn detect_strings(obj: &mut ObjInfo) -> Result<()> {
let mut symbols_set = Vec::<(usize, ObjDataKind, usize)>::new();
let mut symbols_set = Vec::<(SymbolIndex, ObjDataKind, usize)>::new();
for (section_index, section) in obj
.sections
.iter()

View File

@ -7,7 +7,7 @@ use crate::{
analysis::cfa::{AnalyzerState, FunctionInfo, SectionAddress},
obj::{
ObjInfo, ObjKind, ObjRelocKind, ObjSectionKind, ObjSymbol, ObjSymbolFlagSet,
ObjSymbolFlags, ObjSymbolKind,
ObjSymbolFlags, ObjSymbolKind, SectionIndex,
},
};
@ -147,14 +147,14 @@ impl AnalysisPass for FindRelCtorsDtors {
// And the section ends with a null pointer
while let Some(reloc) = obj.unresolved_relocations.iter().find(|reloc| {
reloc.module_id == obj.module_id
&& reloc.section == section.elf_index as u8
&& reloc.section as SectionIndex == section.elf_index
&& reloc.address == current_address
&& reloc.kind == ObjRelocKind::Absolute
}) {
let Some((target_section_index, target_section)) = obj
.sections
.iter()
.find(|(_, section)| section.elf_index == reloc.target_section as usize)
let Some((target_section_index, target_section)) =
obj.sections.iter().find(|(_, section)| {
section.elf_index == reloc.target_section as SectionIndex
})
else {
return false;
};

View File

@ -4,6 +4,7 @@ use std::{
};
use anyhow::{bail, Result};
use cwextab::decode_extab;
use ppc750cl::Opcode;
use tracing::{debug_span, info_span};
use tracing_attributes::instrument;
@ -18,7 +19,7 @@ use crate::{
},
obj::{
ObjDataKind, ObjInfo, ObjKind, ObjReloc, ObjRelocKind, ObjSection, ObjSectionKind,
ObjSymbol, ObjSymbolFlagSet, ObjSymbolFlags, ObjSymbolKind,
ObjSymbol, ObjSymbolFlagSet, ObjSymbolFlags, ObjSymbolKind, SectionIndex, SymbolIndex,
},
};
@ -120,6 +121,7 @@ impl Tracker {
self.process_data(obj, section_index, section)?;
}
}
self.check_extab_relocations(obj)?;
self.reject_invalid_relocations(obj)?;
Ok(())
}
@ -147,6 +149,62 @@ impl Tracker {
Ok(())
}
/// Check all of the extab relocations, and reject any invalid ones by checking against the decoded table data
/// of each table.
fn check_extab_relocations(&mut self, obj: &ObjInfo) -> Result<()> {
let mut to_reject = vec![];
let Some((section_index, section)) = obj.sections.by_name("extab")? else {
// No extab section found, return
return Ok(());
};
let mut decoded_reloc_addrs: BTreeSet<u32> = BTreeSet::new();
// Decode each exception table, and collect all of the relocations from the decoded data for each
for (_, symbol) in obj.symbols.for_section(section_index) {
let extab_name = &symbol.name;
let extab_start_addr: u32 = symbol.address as u32;
let extab_end_addr: u32 = extab_start_addr + symbol.size as u32;
let Ok(extab_data) = section.data_range(extab_start_addr, extab_end_addr) else {
log::warn!("Failed to get extab data for symbol {}", extab_name);
continue;
};
let data = match decode_extab(extab_data) {
Ok(decoded_data) => decoded_data,
Err(e) => {
log::warn!(
"Exception table decoding failed for symbol {}, reason: {}",
extab_name,
e
);
continue;
}
};
for reloc in data.relocations {
let reloc_addr = extab_start_addr + reloc.offset;
decoded_reloc_addrs.insert(reloc_addr);
}
}
let section_start_addr = SectionAddress::new(section_index, section.address as u32);
let section_end_addr = section_start_addr + (section.size as u32);
// Check all the extab relocations against the list of relocations from the decoded tables. Any
// relocations that aren't in the list are invalid, and are removed (if a table fails to decode,
// however, its relocations are all removed).
for (&address, _) in self.relocations.range(section_start_addr..section_end_addr) {
if !decoded_reloc_addrs.contains(&address.address) {
log::debug!("Rejecting invalid extab relocation @ {}", address);
to_reject.push(address);
}
}
for address in to_reject {
self.relocations.remove(&address);
}
Ok(())
}
fn process_code(&mut self, obj: &ObjInfo) -> Result<()> {
if let Some(entry) = obj.entry {
let (section_index, _) = obj.sections.at_address(entry as u32)?;
@ -240,7 +298,7 @@ impl Tracker {
debug_assert_ne!(
value,
RelocationTarget::Address(SectionAddress::new(
usize::MAX,
SectionIndex::MAX,
0
))
);
@ -301,7 +359,7 @@ impl Tracker {
debug_assert_ne!(
address,
RelocationTarget::Address(SectionAddress::new(
usize::MAX,
SectionIndex::MAX,
0
))
);
@ -322,7 +380,7 @@ impl Tracker {
debug_assert_ne!(
address,
RelocationTarget::Address(SectionAddress::new(
usize::MAX,
SectionIndex::MAX,
0
))
);
@ -406,7 +464,7 @@ impl Tracker {
{
(addr, is_function_addr(addr))
} else {
(SectionAddress::new(usize::MAX, 0), false)
(SectionAddress::new(SectionIndex::MAX, 0), false)
};
if branch.link || !is_fn_addr {
self.relocations.insert(ins_addr, match ins.op {
@ -491,7 +549,7 @@ impl Tracker {
fn process_data(
&mut self,
obj: &ObjInfo,
section_index: usize,
section_index: SectionIndex,
section: &ObjSection,
) -> Result<()> {
let mut addr = SectionAddress::new(section_index, section.address as u32);
@ -544,7 +602,7 @@ impl Tracker {
} else {
// Check known relocations (function signature matching)
if self.known_relocations.contains(&from) {
return Some(SectionAddress::new(usize::MAX, addr));
return Some(SectionAddress::new(SectionIndex::MAX, addr));
}
// Check special symbols
if self.stack_address == Some(addr)
@ -555,7 +613,7 @@ impl Tracker {
|| self.sda2_base == Some(addr)
|| self.sda_base == Some(addr)
{
return Some(SectionAddress::new(usize::MAX, addr));
return Some(SectionAddress::new(SectionIndex::MAX, addr));
}
// Not valid
None
@ -567,7 +625,7 @@ impl Tracker {
obj: &mut ObjInfo,
addr: u32,
reloc_kind: ObjRelocKind,
) -> Option<usize> {
) -> Option<SymbolIndex> {
if !matches!(
reloc_kind,
ObjRelocKind::PpcAddr16Ha | ObjRelocKind::PpcAddr16Lo
@ -583,7 +641,7 @@ impl Tracker {
// return generate_special_symbol(obj, addr, &name).ok();
// }
// }
let mut check_symbol = |opt: Option<u32>, name: &str| -> Option<usize> {
let mut check_symbol = |opt: Option<u32>, name: &str| -> Option<SymbolIndex> {
if let Some(value) = opt {
if addr == value {
return generate_special_symbol(obj, value, name).ok();
@ -808,7 +866,7 @@ fn data_kind_from_op(op: Opcode) -> DataKind {
}
}
fn generate_special_symbol(obj: &mut ObjInfo, addr: u32, name: &str) -> Result<usize> {
fn generate_special_symbol(obj: &mut ObjInfo, addr: u32, name: &str) -> Result<SymbolIndex> {
obj.add_symbol(
ObjSymbol {
name: name.to_string(),

View File

@ -208,11 +208,11 @@ impl VM {
(
GprValue::Address(RelocationTarget::Address(left)),
GprValue::Constant(right),
) => GprValue::Address(RelocationTarget::Address(left + right)),
) => GprValue::Address(RelocationTarget::Address(left.wrapping_add(right))),
(
GprValue::Constant(left),
GprValue::Address(RelocationTarget::Address(right)),
) => GprValue::Address(RelocationTarget::Address(right + left)),
) => GprValue::Address(RelocationTarget::Address(right.wrapping_add(left))),
_ => GprValue::Unknown,
};
self.gpr[ins.field_rd() as usize].set_direct(value);

View File

@ -1,18 +1,18 @@
use std::{
io::{stdout, Write},
path::PathBuf,
};
use std::io::{stdout, Write};
use anyhow::Result;
use argp::FromArgs;
use typed_path::Utf8NativePathBuf;
use crate::{
cmd,
util::{
alf::AlfFile,
file::{buf_writer, map_file},
file::buf_writer,
path::native_path,
reader::{Endian, FromReader},
},
vfs::open_file,
};
#[derive(FromArgs, PartialEq, Debug)]
@ -34,21 +34,21 @@ enum SubCommand {
/// Prints information about an alf file. (Same as `dol info`)
#[argp(subcommand, name = "info")]
pub struct InfoArgs {
#[argp(positional)]
#[argp(positional, from_str_fn(native_path))]
/// alf file
file: PathBuf,
file: Utf8NativePathBuf,
}
#[derive(FromArgs, PartialEq, Debug)]
/// Extracts symbol hashes from an alf file.
#[argp(subcommand, name = "hashes")]
pub struct HashesArgs {
#[argp(positional)]
#[argp(positional, from_str_fn(native_path))]
/// alf file
alf_file: PathBuf,
#[argp(positional)]
alf_file: Utf8NativePathBuf,
#[argp(positional, from_str_fn(native_path))]
/// output file
output: Option<PathBuf>,
output: Option<Utf8NativePathBuf>,
}
pub fn run(args: Args) -> Result<()> {
@ -60,11 +60,10 @@ pub fn run(args: Args) -> Result<()> {
fn hashes(args: HashesArgs) -> Result<()> {
let alf_file = {
let file = map_file(&args.alf_file)?;
let mut reader = file.as_reader();
AlfFile::from_reader(&mut reader, Endian::Little)?
let mut file = open_file(&args.alf_file, true)?;
AlfFile::from_reader(file.as_mut(), Endian::Little)?
};
let mut w: Box<dyn Write> = if let Some(output) = args.output {
let mut w: Box<dyn Write> = if let Some(output) = &args.output {
Box::new(buf_writer(output)?)
} else {
Box::new(stdout())

View File

@ -2,14 +2,20 @@ use std::{
collections::{btree_map::Entry, BTreeMap},
fs::File,
io::Write,
path::PathBuf,
};
use anyhow::{anyhow, bail, Context, Result};
use argp::FromArgs;
use object::{Object, ObjectSymbol, SymbolScope};
use typed_path::Utf8NativePathBuf;
use crate::util::file::{buf_writer, map_file, map_file_basic, process_rsp};
use crate::{
util::{
file::{buf_writer, process_rsp},
path::native_path,
},
vfs::open_file,
};
#[derive(FromArgs, PartialEq, Debug)]
/// Commands for processing static libraries.
@ -30,24 +36,24 @@ enum SubCommand {
/// Creates a static library.
#[argp(subcommand, name = "create")]
pub struct CreateArgs {
#[argp(positional)]
#[argp(positional, from_str_fn(native_path))]
/// output file
out: PathBuf,
#[argp(positional)]
out: Utf8NativePathBuf,
#[argp(positional, from_str_fn(native_path))]
/// input files
files: Vec<PathBuf>,
files: Vec<Utf8NativePathBuf>,
}
#[derive(FromArgs, PartialEq, Eq, Debug)]
/// Extracts a static library.
#[argp(subcommand, name = "extract")]
pub struct ExtractArgs {
#[argp(positional)]
#[argp(positional, from_str_fn(native_path))]
/// input files
files: Vec<PathBuf>,
#[argp(option, short = 'o')]
files: Vec<Utf8NativePathBuf>,
#[argp(option, short = 'o', from_str_fn(native_path))]
/// output directory
out: Option<PathBuf>,
out: Option<Utf8NativePathBuf>,
#[argp(switch, short = 'q')]
/// quiet output
quiet: bool,
@ -71,17 +77,16 @@ fn create(args: CreateArgs) -> Result<()> {
let mut identifiers = Vec::with_capacity(files.len());
let mut symbol_table = BTreeMap::new();
for path in &files {
let path_str =
path.to_str().ok_or_else(|| anyhow!("'{}' is not valid UTF-8", path.display()))?;
let identifier = path_str.as_bytes().to_vec();
let unix_path = path.with_unix_encoding();
let identifier = unix_path.as_str().as_bytes().to_vec();
identifiers.push(identifier.clone());
let entries = match symbol_table.entry(identifier) {
Entry::Vacant(e) => e.insert(Vec::new()),
Entry::Occupied(_) => bail!("Duplicate file name '{path_str}'"),
Entry::Occupied(_) => bail!("Duplicate file name '{unix_path}'"),
};
let file = map_file_basic(path)?;
let obj = object::File::parse(file.as_slice())?;
let mut file = open_file(path, false)?;
let obj = object::File::parse(file.map()?)?;
for symbol in obj.symbols() {
if symbol.scope() == SymbolScope::Dynamic {
entries.push(symbol.name_bytes()?.to_vec());
@ -99,10 +104,8 @@ fn create(args: CreateArgs) -> Result<()> {
symbol_table,
)?;
for path in files {
let path_str =
path.to_str().ok_or_else(|| anyhow!("'{}' is not valid UTF-8", path.display()))?;
let mut file = File::open(&path)?;
builder.append_file(path_str.as_bytes(), &mut file)?;
builder.append_file(path.as_str().as_bytes(), &mut file)?;
}
builder.into_inner()?.flush()?;
Ok(())
@ -115,7 +118,8 @@ fn extract(args: ExtractArgs) -> Result<()> {
// Extract files
let mut num_files = 0;
for path in &files {
let mut out_dir = if let Some(out) = &args.out { out.clone() } else { PathBuf::new() };
let mut out_dir =
if let Some(out) = &args.out { out.clone() } else { Utf8NativePathBuf::new() };
// If there are multiple files, extract to separate directories
if files.len() > 1 {
out_dir
@ -123,14 +127,13 @@ fn extract(args: ExtractArgs) -> Result<()> {
}
std::fs::create_dir_all(&out_dir)?;
if !args.quiet {
println!("Extracting {} to {}", path.display(), out_dir.display());
println!("Extracting {} to {}", path, out_dir);
}
let file = map_file(path)?;
let mut archive = ar::Archive::new(file.as_slice());
let mut file = open_file(path, false)?;
let mut archive = ar::Archive::new(file.map()?);
while let Some(entry) = archive.next_entry() {
let mut entry =
entry.with_context(|| format!("Processing entry in {}", path.display()))?;
let mut entry = entry.with_context(|| format!("Processing entry in {}", path))?;
let file_name = std::str::from_utf8(entry.header().identifier())?;
if !args.quiet && args.verbose {
println!("\t{}", file_name);
@ -143,7 +146,7 @@ fn extract(args: ExtractArgs) -> Result<()> {
std::fs::create_dir_all(parent)?;
}
let mut file = File::create(&file_path)
.with_context(|| format!("Failed to create file {}", file_path.display()))?;
.with_context(|| format!("Failed to create file {}", file_path))?;
std::io::copy(&mut entry, &mut file)?;
file.flush()?;

File diff suppressed because it is too large Load Diff

View File

@ -2,7 +2,6 @@ use std::{
collections::{btree_map, BTreeMap},
io::{stdout, Cursor, Read, Write},
ops::Bound::{Excluded, Unbounded},
path::PathBuf,
str::from_utf8,
};
@ -15,13 +14,18 @@ use syntect::{
highlighting::{Color, HighlightIterator, HighlightState, Highlighter, Theme, ThemeSet},
parsing::{ParseState, ScopeStack, SyntaxReference, SyntaxSet},
};
use typed_path::Utf8NativePathBuf;
use crate::util::{
dwarf::{
process_compile_unit, process_cu_tag, process_overlay_branch, read_debug_section,
should_skip_tag, tag_type_string, AttributeKind, TagKind,
use crate::{
util::{
dwarf::{
process_compile_unit, process_cu_tag, process_overlay_branch, read_debug_section,
should_skip_tag, tag_type_string, AttributeKind, TagKind,
},
file::buf_writer,
path::native_path,
},
file::{buf_writer, map_file},
vfs::open_file,
};
#[derive(FromArgs, PartialEq, Debug)]
@ -42,12 +46,12 @@ enum SubCommand {
/// Dumps DWARF 1.1 info from an object or archive.
#[argp(subcommand, name = "dump")]
pub struct DumpArgs {
#[argp(positional)]
#[argp(positional, from_str_fn(native_path))]
/// Input object. (ELF or archive)
in_file: PathBuf,
#[argp(option, short = 'o')]
in_file: Utf8NativePathBuf,
#[argp(option, short = 'o', from_str_fn(native_path))]
/// Output file. (Or directory, for archive)
out: Option<PathBuf>,
out: Option<Utf8NativePathBuf>,
#[argp(switch)]
/// Disable color output.
no_color: bool,
@ -73,8 +77,8 @@ fn dump(args: DumpArgs) -> Result<()> {
let theme = theme_set.themes.get("Solarized (dark)").context("Failed to load theme")?;
let syntax = syntax_set.find_syntax_by_name("C++").context("Failed to find syntax")?.clone();
let file = map_file(&args.in_file)?;
let buf = file.as_slice();
let mut file = open_file(&args.in_file, true)?;
let buf = file.map()?;
if buf.starts_with(b"!<arch>\n") {
let mut archive = ar::Archive::new(buf);
while let Some(result) = archive.next_entry() {
@ -101,7 +105,7 @@ fn dump(args: DumpArgs) -> Result<()> {
let name = name.trim_start_matches("D:").replace('\\', "/");
let name = name.rsplit_once('/').map(|(_, b)| b).unwrap_or(&name);
let file_path = out_path.join(format!("{}.txt", name));
let mut file = buf_writer(file_path)?;
let mut file = buf_writer(&file_path)?;
dump_debug_section(&args, &mut file, &obj_file, debug_section)?;
file.flush()?;
} else if args.no_color {

View File

@ -3,7 +3,6 @@ use std::{
fs,
fs::DirBuilder,
io::{Cursor, Write},
path::PathBuf,
};
use anyhow::{anyhow, bail, ensure, Context, Result};
@ -15,6 +14,7 @@ use object::{
FileFlags, Object, ObjectSection, ObjectSymbol, RelocationTarget, SectionFlags, SectionIndex,
SectionKind, SymbolFlags, SymbolIndex, SymbolKind, SymbolScope, SymbolSection,
};
use typed_path::{Utf8NativePath, Utf8NativePathBuf};
use crate::{
obj::ObjKind,
@ -24,6 +24,7 @@ use crate::{
config::{write_splits_file, write_symbols_file},
elf::{process_elf, write_elf},
file::{buf_writer, process_rsp},
path::native_path,
reader::{Endian, FromReader},
signatures::{compare_signature, generate_signature, FunctionSignature},
split::split_obj,
@ -54,72 +55,72 @@ enum SubCommand {
/// Disassembles an ELF file.
#[argp(subcommand, name = "disasm")]
pub struct DisasmArgs {
#[argp(positional)]
#[argp(positional, from_str_fn(native_path))]
/// input file
elf_file: PathBuf,
#[argp(positional)]
elf_file: Utf8NativePathBuf,
#[argp(positional, from_str_fn(native_path))]
/// output file (.o) or directory (.elf)
out: PathBuf,
out: Utf8NativePathBuf,
}
#[derive(FromArgs, PartialEq, Eq, Debug)]
/// Fixes issues with GNU assembler built object files.
#[argp(subcommand, name = "fixup")]
pub struct FixupArgs {
#[argp(positional)]
#[argp(positional, from_str_fn(native_path))]
/// input file
in_file: PathBuf,
#[argp(positional)]
in_file: Utf8NativePathBuf,
#[argp(positional, from_str_fn(native_path))]
/// output file
out_file: PathBuf,
out_file: Utf8NativePathBuf,
}
#[derive(FromArgs, PartialEq, Eq, Debug)]
/// Splits an executable ELF into relocatable objects.
#[argp(subcommand, name = "split")]
pub struct SplitArgs {
#[argp(positional)]
#[argp(positional, from_str_fn(native_path))]
/// input file
in_file: PathBuf,
#[argp(positional)]
in_file: Utf8NativePathBuf,
#[argp(positional, from_str_fn(native_path))]
/// output directory
out_dir: PathBuf,
out_dir: Utf8NativePathBuf,
}
#[derive(FromArgs, PartialEq, Eq, Debug)]
/// Generates configuration files from an executable ELF.
#[argp(subcommand, name = "config")]
pub struct ConfigArgs {
#[argp(positional)]
#[argp(positional, from_str_fn(native_path))]
/// input file
in_file: PathBuf,
#[argp(positional)]
in_file: Utf8NativePathBuf,
#[argp(positional, from_str_fn(native_path))]
/// output directory
out_dir: PathBuf,
out_dir: Utf8NativePathBuf,
}
#[derive(FromArgs, PartialEq, Eq, Debug)]
/// Builds function signatures from an ELF file.
#[argp(subcommand, name = "sigs")]
pub struct SignaturesArgs {
#[argp(positional)]
#[argp(positional, from_str_fn(native_path))]
/// input file(s)
files: Vec<PathBuf>,
files: Vec<Utf8NativePathBuf>,
#[argp(option, short = 's')]
/// symbol name
symbol: String,
#[argp(option, short = 'o')]
#[argp(option, short = 'o', from_str_fn(native_path))]
/// output yml
out_file: PathBuf,
out_file: Utf8NativePathBuf,
}
#[derive(FromArgs, PartialEq, Eq, Debug)]
/// Prints information about an ELF file.
#[argp(subcommand, name = "info")]
pub struct InfoArgs {
#[argp(positional)]
#[argp(positional, from_str_fn(native_path))]
/// input file
input: PathBuf,
input: Utf8NativePathBuf,
}
pub fn run(args: Args) -> Result<()> {
@ -134,17 +135,17 @@ pub fn run(args: Args) -> Result<()> {
}
fn config(args: ConfigArgs) -> Result<()> {
log::info!("Loading {}", args.in_file.display());
log::info!("Loading {}", args.in_file);
let obj = process_elf(&args.in_file)?;
DirBuilder::new().recursive(true).create(&args.out_dir)?;
write_symbols_file(args.out_dir.join("symbols.txt"), &obj, None)?;
write_splits_file(args.out_dir.join("splits.txt"), &obj, false, None)?;
write_symbols_file(&args.out_dir.join("symbols.txt"), &obj, None)?;
write_splits_file(&args.out_dir.join("splits.txt"), &obj, false, None)?;
Ok(())
}
fn disasm(args: DisasmArgs) -> Result<()> {
log::info!("Loading {}", args.elf_file.display());
log::info!("Loading {}", args.elf_file);
let obj = process_elf(&args.elf_file)?;
match obj.kind {
ObjKind::Executable => {
@ -156,12 +157,12 @@ fn disasm(args: DisasmArgs) -> Result<()> {
DirBuilder::new().recursive(true).create(&include_dir)?;
fs::write(include_dir.join("macros.inc"), include_bytes!("../../assets/macros.inc"))?;
let mut files_out = buf_writer(args.out.join("link_order.txt"))?;
let mut files_out = buf_writer(&args.out.join("link_order.txt"))?;
for (unit, split_obj) in obj.link_order.iter().zip(&split_objs) {
let out_path = asm_dir.join(file_name_from_unit(&unit.name, ".s"));
log::info!("Writing {}", out_path.display());
log::info!("Writing {}", out_path);
let mut w = buf_writer(out_path)?;
let mut w = buf_writer(&out_path)?;
write_asm(&mut w, split_obj)?;
w.flush()?;
@ -170,7 +171,7 @@ fn disasm(args: DisasmArgs) -> Result<()> {
files_out.flush()?;
}
ObjKind::Relocatable => {
let mut w = buf_writer(args.out)?;
let mut w = buf_writer(&args.out)?;
write_asm(&mut w, &obj)?;
w.flush()?;
}
@ -193,18 +194,17 @@ fn split(args: SplitArgs) -> Result<()> {
};
}
let mut rsp_file = buf_writer("rsp")?;
let mut rsp_file = buf_writer(Utf8NativePath::new("rsp"))?;
for unit in &obj.link_order {
let object = file_map
.get(&unit.name)
.ok_or_else(|| anyhow!("Failed to find object file for unit '{}'", unit.name))?;
let out_path = args.out_dir.join(file_name_from_unit(&unit.name, ".o"));
writeln!(rsp_file, "{}", out_path.display())?;
writeln!(rsp_file, "{}", out_path)?;
if let Some(parent) = out_path.parent() {
DirBuilder::new().recursive(true).create(parent)?;
}
fs::write(&out_path, object)
.with_context(|| format!("Failed to write '{}'", out_path.display()))?;
fs::write(&out_path, object).with_context(|| format!("Failed to write '{}'", out_path))?;
}
rsp_file.flush()?;
Ok(())
@ -237,7 +237,7 @@ const ASM_SUFFIX: &str = " (asm)";
fn fixup(args: FixupArgs) -> Result<()> {
let in_buf = fs::read(&args.in_file)
.with_context(|| format!("Failed to open input file: '{}'", args.in_file.display()))?;
.with_context(|| format!("Failed to open input file: '{}'", args.in_file))?;
let in_file = object::read::File::parse(&*in_buf).context("Failed to parse input ELF")?;
let mut out_file =
object::write::Object::new(in_file.format(), in_file.architecture(), in_file.endianness());
@ -262,10 +262,7 @@ fn fixup(args: FixupArgs) -> Result<()> {
let file_name = args
.in_file
.file_name()
.ok_or_else(|| anyhow!("'{}' is not a file path", args.in_file.display()))?;
let file_name = file_name
.to_str()
.ok_or_else(|| anyhow!("'{}' is not valid UTF-8", file_name.to_string_lossy()))?;
.ok_or_else(|| anyhow!("'{}' is not a file path", args.in_file))?;
let mut name_bytes = file_name.as_bytes().to_vec();
name_bytes.append(&mut ASM_SUFFIX.as_bytes().to_vec());
out_file.add_symbol(object::write::Symbol {
@ -281,7 +278,7 @@ fn fixup(args: FixupArgs) -> Result<()> {
}
// Write section symbols & sections
let mut section_ids: Vec<Option<SectionId>> = vec![];
let mut section_ids: Vec<Option<SectionId>> = vec![None /* ELF null section */];
for section in in_file.sections() {
// Skip empty sections or metadata sections
if section.size() == 0 || section.kind() == SectionKind::Metadata {
@ -304,11 +301,11 @@ fn fixup(args: FixupArgs) -> Result<()> {
}
// Write symbols
let mut symbol_ids: Vec<Option<SymbolId>> = vec![];
let mut symbol_ids: Vec<Option<SymbolId>> = vec![None /* ELF null symbol */];
let mut addr_to_sym: BTreeMap<SectionId, BTreeMap<u32, SymbolId>> = BTreeMap::new();
for symbol in in_file.symbols() {
// Skip section and file symbols, we wrote them above
if matches!(symbol.kind(), SymbolKind::Section | SymbolKind::File | SymbolKind::Null) {
if matches!(symbol.kind(), SymbolKind::Section | SymbolKind::File) {
symbol_ids.push(None);
continue;
}
@ -445,7 +442,7 @@ fn signatures(args: SignaturesArgs) -> Result<()> {
let mut signatures: HashMap<String, FunctionSignature> = HashMap::new();
for path in files {
log::info!("Processing {}", path.display());
log::info!("Processing {}", path);
let signature = match generate_signature(&path, &args.symbol) {
Ok(Some(signature)) => signature,
Ok(None) => continue,
@ -472,7 +469,7 @@ fn signatures(args: SignaturesArgs) -> Result<()> {
fn info(args: InfoArgs) -> Result<()> {
let in_buf = fs::read(&args.input)
.with_context(|| format!("Failed to open input file: '{}'", args.input.display()))?;
.with_context(|| format!("Failed to open input file: '{}'", args.input))?;
let in_file = object::read::File::parse(&*in_buf).context("Failed to parse input ELF")?;
println!("ELF type: {:?}", in_file.kind());
@ -488,7 +485,7 @@ fn info(args: InfoArgs) -> Result<()> {
"{: >15} | {: <10} | {: <10} | {: <10} | {: <10}",
"Name", "Type", "Size", "File Off", "Index"
);
for section in in_file.sections().skip(1) {
for section in in_file.sections() {
let kind_str = match section.kind() {
SectionKind::Text => "code".to_cow(),
SectionKind::Data => "data".to_cow(),
@ -565,6 +562,7 @@ fn info(args: InfoArgs) -> Result<()> {
);
println!("\tUnsafe global reg vars: {}", header.unsafe_global_reg_vars);
println!("\n{: >10} | {: <6} | {: <6} | {: <10}", "Align", "Vis", "Active", "Symbol");
CommentSym::from_reader(&mut reader, Endian::Big)?; // ELF null symbol
for symbol in in_file.symbols() {
let comment_sym = CommentSym::from_reader(&mut reader, Endian::Big)?;
if symbol.is_definition() {
@ -603,7 +601,7 @@ fn info(args: InfoArgs) -> Result<()> {
if let Some(virtual_addresses) = &meta.virtual_addresses {
println!("\tVirtual addresses:");
println!("\t{: >10} | {: <10}", "Addr", "Symbol");
for (symbol, addr) in in_file.symbols().zip(virtual_addresses) {
for (symbol, addr) in in_file.symbols().zip(virtual_addresses.iter().skip(1)) {
if symbol.is_definition() {
println!("\t{: >10} | {: <10}", format!("{:#X}", addr), symbol.name()?);
}

View File

@ -1,24 +1,28 @@
use std::{
io::{Seek, SeekFrom, Write},
path::PathBuf,
};
use std::io::{Seek, SeekFrom, Write};
use anyhow::{anyhow, bail, ensure, Result};
use argp::FromArgs;
use object::{Architecture, Endianness, Object, ObjectKind, ObjectSection, SectionKind};
use typed_path::Utf8NativePathBuf;
use crate::util::file::{buf_writer, map_file};
use crate::{
util::{file::buf_writer, path::native_path},
vfs::open_file,
};
#[derive(FromArgs, PartialEq, Eq, Debug)]
/// Converts an ELF file to a DOL file.
#[argp(subcommand, name = "elf2dol")]
pub struct Args {
#[argp(positional)]
#[argp(positional, from_str_fn(native_path))]
/// path to input ELF
elf_file: PathBuf,
#[argp(positional)]
elf_file: Utf8NativePathBuf,
#[argp(positional, from_str_fn(native_path))]
/// path to output DOL
dol_file: PathBuf,
dol_file: Utf8NativePathBuf,
/// sections (by name) to ignore
#[argp(option, long = "ignore")]
deny_sections: Vec<String>,
}
#[derive(Debug, Clone, Default)]
@ -43,8 +47,8 @@ const MAX_TEXT_SECTIONS: usize = 7;
const MAX_DATA_SECTIONS: usize = 11;
pub fn run(args: Args) -> Result<()> {
let file = map_file(&args.elf_file)?;
let obj_file = object::read::File::parse(file.as_slice())?;
let mut file = open_file(&args.elf_file, true)?;
let obj_file = object::read::File::parse(file.map()?)?;
match obj_file.architecture() {
Architecture::PowerPc => {}
arch => bail!("Unexpected architecture: {arch:?}"),
@ -61,9 +65,11 @@ pub fn run(args: Args) -> Result<()> {
out.seek(SeekFrom::Start(offset as u64))?;
// Text sections
for section in
obj_file.sections().filter(|s| section_kind(s) == SectionKind::Text && is_alloc(s.flags()))
{
for section in obj_file.sections().filter(|s| {
section_kind(s) == SectionKind::Text
&& is_alloc(s.flags())
&& is_name_allowed(s, &args.deny_sections)
}) {
log::debug!("Processing text section '{}'", section.name().unwrap_or("[error]"));
let address = section.address() as u32;
let size = align32(section.size() as u32);
@ -79,9 +85,11 @@ pub fn run(args: Args) -> Result<()> {
}
// Data sections
for section in
obj_file.sections().filter(|s| section_kind(s) == SectionKind::Data && is_alloc(s.flags()))
{
for section in obj_file.sections().filter(|s| {
section_kind(s) == SectionKind::Data
&& is_alloc(s.flags())
&& is_name_allowed(s, &args.deny_sections)
}) {
log::debug!("Processing data section '{}'", section.name().unwrap_or("[error]"));
let address = section.address() as u32;
let size = align32(section.size() as u32);
@ -97,10 +105,11 @@ pub fn run(args: Args) -> Result<()> {
}
// BSS sections
for section in obj_file
.sections()
.filter(|s| section_kind(s) == SectionKind::UninitializedData && is_alloc(s.flags()))
{
for section in obj_file.sections().filter(|s| {
section_kind(s) == SectionKind::UninitializedData
&& is_alloc(s.flags())
&& is_name_allowed(s, &args.deny_sections)
}) {
let address = section.address() as u32;
let size = section.size() as u32;
if header.bss_address == 0 {
@ -184,3 +193,8 @@ fn section_kind(section: &object::Section) -> SectionKind {
fn is_alloc(flags: object::SectionFlags) -> bool {
matches!(flags, object::SectionFlags::Elf { sh_flags } if sh_flags & object::elf::SHF_ALLOC as u64 != 0)
}
#[inline]
fn is_name_allowed(s: &object::Section, denied: &[String]) -> bool {
!denied.contains(&s.name().unwrap_or("[error]").to_string())
}

View File

@ -1,12 +1,19 @@
use std::path::PathBuf;
use std::fs::DirBuilder;
use anyhow::{bail, ensure, Result};
use argp::FromArgs;
use cwdemangle::{demangle, DemangleOptions};
use tracing::error;
use typed_path::Utf8NativePathBuf;
use crate::util::{
file::map_file,
map::{process_map, SymbolEntry, SymbolRef},
use crate::{
util::{
config::{write_splits_file, write_symbols_file},
map::{create_obj, process_map, SymbolEntry, SymbolRef},
path::native_path,
split::update_splits,
},
vfs::open_file,
};
#[derive(FromArgs, PartialEq, Debug)]
@ -22,15 +29,16 @@ pub struct Args {
enum SubCommand {
Entries(EntriesArgs),
Symbol(SymbolArgs),
Config(ConfigArgs),
}
#[derive(FromArgs, PartialEq, Eq, Debug)]
/// Displays all entries for a particular TU.
#[argp(subcommand, name = "entries")]
pub struct EntriesArgs {
#[argp(positional)]
#[argp(positional, from_str_fn(native_path))]
/// path to input map
map_file: PathBuf,
map_file: Utf8NativePathBuf,
#[argp(positional)]
/// TU to display entries for
unit: String,
@ -40,24 +48,37 @@ pub struct EntriesArgs {
/// Displays all references to a symbol.
#[argp(subcommand, name = "symbol")]
pub struct SymbolArgs {
#[argp(positional)]
#[argp(positional, from_str_fn(native_path))]
/// path to input map
map_file: PathBuf,
map_file: Utf8NativePathBuf,
#[argp(positional)]
/// symbol to display references for
symbol: String,
}
#[derive(FromArgs, PartialEq, Eq, Debug)]
/// Generates project configuration files from a map. (symbols.txt, splits.txt)
#[argp(subcommand, name = "config")]
pub struct ConfigArgs {
#[argp(positional, from_str_fn(native_path))]
/// path to input map
map_file: Utf8NativePathBuf,
#[argp(positional, from_str_fn(native_path))]
/// output directory for symbols.txt and splits.txt
out_dir: Utf8NativePathBuf,
}
pub fn run(args: Args) -> Result<()> {
match args.command {
SubCommand::Entries(c_args) => entries(c_args),
SubCommand::Symbol(c_args) => symbol(c_args),
SubCommand::Config(c_args) => config(c_args),
}
}
fn entries(args: EntriesArgs) -> Result<()> {
let file = map_file(&args.map_file)?;
let entries = process_map(&mut file.as_reader(), None, None)?;
let mut file = open_file(&args.map_file, true)?;
let entries = process_map(file.as_mut(), None, None)?;
match entries.unit_entries.get_vec(&args.unit) {
Some(vec) => {
println!("Entries for {}:", args.unit);
@ -87,9 +108,9 @@ fn entries(args: EntriesArgs) -> Result<()> {
}
fn symbol(args: SymbolArgs) -> Result<()> {
let file = map_file(&args.map_file)?;
let mut file = open_file(&args.map_file, true)?;
log::info!("Processing map...");
let entries = process_map(&mut file.as_reader(), None, None)?;
let entries = process_map(file.as_mut(), None, None)?;
log::info!("Done!");
let mut opt_ref: Option<(String, SymbolEntry)> = None;
@ -160,3 +181,18 @@ fn symbol(args: SymbolArgs) -> Result<()> {
println!("\n");
Ok(())
}
fn config(args: ConfigArgs) -> Result<()> {
let mut file = open_file(&args.map_file, true)?;
log::info!("Processing map...");
let entries = process_map(file.as_mut(), None, None)?;
let mut obj = create_obj(&entries)?;
if let Err(e) = update_splits(&mut obj, None, false) {
error!("Failed to update splits: {}", e)
}
DirBuilder::new().recursive(true).create(&args.out_dir)?;
write_symbols_file(&args.out_dir.join("symbols.txt"), &obj, None)?;
write_splits_file(&args.out_dir.join("splits.txt"), &obj, false, None)?;
log::info!("Done!");
Ok(())
}

View File

@ -1,48 +0,0 @@
use std::path::PathBuf;
use anyhow::{bail, ensure, Context, Result};
use argp::FromArgs;
use memchr::memmem;
use memmap2::MmapOptions;
#[derive(FromArgs, PartialEq, Eq, Debug)]
/// Sets the MetroidBuildInfo tag value in a given binary.
#[argp(subcommand, name = "metroidbuildinfo")]
pub struct Args {
#[argp(positional)]
/// path to source binary
binary: PathBuf,
#[argp(positional)]
/// path to build info string
build_info: PathBuf,
}
const BUILD_STRING_MAX: usize = 35;
const BUILD_STRING_TAG: &str = "!#$MetroidBuildInfo!#$";
pub fn run(args: Args) -> Result<()> {
let build_string = std::fs::read_to_string(&args.build_info).with_context(|| {
format!("Failed to read build info string from '{}'", args.build_info.display())
})?;
let build_string_trim = build_string.trim_end();
let build_string_bytes = build_string_trim.as_bytes();
ensure!(
build_string_bytes.len() <= BUILD_STRING_MAX,
"Build string '{build_string_trim}' is greater than maximum size of {BUILD_STRING_MAX}"
);
let binary_file =
std::fs::File::options().read(true).write(true).open(&args.binary).with_context(|| {
format!("Failed to open binary for writing: '{}'", args.binary.display())
})?;
let mut map = unsafe { MmapOptions::new().map_mut(&binary_file) }
.with_context(|| format!("Failed to mmap binary: '{}'", args.binary.display()))?;
let start = match memmem::find(&map, BUILD_STRING_TAG.as_bytes()) {
Some(idx) => idx + BUILD_STRING_TAG.as_bytes().len(),
None => bail!("Failed to find build string tag in binary"),
};
let end = start + build_string_bytes.len();
map[start..end].copy_from_slice(build_string_bytes);
map[end] = 0;
Ok(())
}

View File

@ -7,12 +7,12 @@ pub mod dwarf;
pub mod elf;
pub mod elf2dol;
pub mod map;
pub mod metroidbuildinfo;
pub mod nlzss;
pub mod rarc;
pub mod rel;
pub mod rso;
pub mod shasum;
pub mod u8_arc;
pub mod vfs;
pub mod yay0;
pub mod yaz0;

View File

@ -1,11 +1,12 @@
use std::{fs, path::PathBuf};
use std::fs;
use anyhow::{anyhow, Context, Result};
use argp::FromArgs;
use typed_path::Utf8NativePathBuf;
use crate::util::{
file::{open_file, process_rsp},
IntoCow, ToCow,
use crate::{
util::{file::process_rsp, nlzss, path::native_path, IntoCow, ToCow},
vfs::open_file,
};
#[derive(FromArgs, PartialEq, Debug)]
@ -26,13 +27,13 @@ enum SubCommand {
/// Decompresses NLZSS-compressed files.
#[argp(subcommand, name = "decompress")]
pub struct DecompressArgs {
#[argp(positional)]
#[argp(positional, from_str_fn(native_path))]
/// NLZSS-compressed file(s)
files: Vec<PathBuf>,
#[argp(option, short = 'o')]
files: Vec<Utf8NativePathBuf>,
#[argp(option, short = 'o', from_str_fn(native_path))]
/// Output file (or directory, if multiple files are specified).
/// If not specified, decompresses in-place.
output: Option<PathBuf>,
output: Option<Utf8NativePathBuf>,
}
pub fn run(args: Args) -> Result<()> {
@ -45,8 +46,9 @@ fn decompress(args: DecompressArgs) -> Result<()> {
let files = process_rsp(&args.files)?;
let single_file = files.len() == 1;
for path in files {
let data = nintendo_lz::decompress(&mut open_file(&path)?)
.map_err(|e| anyhow!("Failed to decompress '{}' with NLZSS: {}", path.display(), e))?;
let mut file = open_file(&path, false)?;
let data = nlzss::decompress(file.as_mut())
.map_err(|e| anyhow!("Failed to decompress '{}' with NLZSS: {}", path, e))?;
let out_path = if let Some(output) = &args.output {
if single_file {
output.as_path().to_cow()
@ -57,7 +59,7 @@ fn decompress(args: DecompressArgs) -> Result<()> {
path.as_path().to_cow()
};
fs::write(out_path.as_ref(), data)
.with_context(|| format!("Failed to write '{}'", out_path.display()))?;
.with_context(|| format!("Failed to write '{}'", out_path))?;
}
Ok(())
}

View File

@ -1,12 +1,9 @@
use std::{fs, fs::DirBuilder, path::PathBuf};
use anyhow::{Context, Result};
use anyhow::Result;
use argp::FromArgs;
use typed_path::Utf8NativePathBuf;
use crate::util::{
file::{decompress_if_needed, map_file},
rarc::{Node, RarcReader},
};
use super::vfs;
use crate::util::path::native_path;
#[derive(FromArgs, PartialEq, Debug)]
/// Commands for processing RSO files.
@ -27,23 +24,29 @@ enum SubCommand {
/// Views RARC file information.
#[argp(subcommand, name = "list")]
pub struct ListArgs {
#[argp(positional)]
#[argp(positional, from_str_fn(native_path))]
/// RARC file
file: PathBuf,
file: Utf8NativePathBuf,
#[argp(switch, short = 's')]
/// Only print filenames.
short: bool,
}
#[derive(FromArgs, PartialEq, Eq, Debug)]
/// Extracts RARC file contents.
#[argp(subcommand, name = "extract")]
pub struct ExtractArgs {
#[argp(positional)]
#[argp(positional, from_str_fn(native_path))]
/// RARC file
file: PathBuf,
#[argp(option, short = 'o')]
file: Utf8NativePathBuf,
#[argp(option, short = 'o', from_str_fn(native_path))]
/// output directory
output: Option<PathBuf>,
output: Option<Utf8NativePathBuf>,
#[argp(switch)]
/// Do not decompress files when copying.
no_decompress: bool,
#[argp(switch, short = 'q')]
/// quiet output
/// Quiet output. Don't print anything except errors.
quiet: bool,
}
@ -55,71 +58,16 @@ pub fn run(args: Args) -> Result<()> {
}
fn list(args: ListArgs) -> Result<()> {
let file = map_file(&args.file)?;
let rarc = RarcReader::new(&mut file.as_reader())
.with_context(|| format!("Failed to process RARC file '{}'", args.file.display()))?;
let mut current_path = PathBuf::new();
for node in rarc.nodes() {
match node {
Node::DirectoryBegin { name } => {
current_path.push(name.name);
}
Node::DirectoryEnd { name: _ } => {
current_path.pop();
}
Node::File { name, offset, size } => {
let path = current_path.join(name.name);
println!("{}: {} bytes, offset {:#X}", path.display(), size, offset);
}
Node::CurrentDirectory => {}
Node::ParentDirectory => {}
}
}
Ok(())
let path = Utf8NativePathBuf::from(format!("{}:", args.file));
vfs::ls(vfs::LsArgs { path, short: args.short, recursive: true })
}
fn extract(args: ExtractArgs) -> Result<()> {
let file = map_file(&args.file)?;
let rarc = RarcReader::new(&mut file.as_reader())
.with_context(|| format!("Failed to process RARC file '{}'", args.file.display()))?;
let mut current_path = PathBuf::new();
for node in rarc.nodes() {
match node {
Node::DirectoryBegin { name } => {
current_path.push(name.name);
}
Node::DirectoryEnd { name: _ } => {
current_path.pop();
}
Node::File { name, offset, size } => {
let file_data = decompress_if_needed(
&file.as_slice()[offset as usize..offset as usize + size as usize],
)?;
let file_path = current_path.join(&name.name);
let output_path = args
.output
.as_ref()
.map(|p| p.join(&file_path))
.unwrap_or_else(|| file_path.clone());
if !args.quiet {
println!(
"Extracting {} to {} ({} bytes)",
file_path.display(),
output_path.display(),
size
);
}
if let Some(parent) = output_path.parent() {
DirBuilder::new().recursive(true).create(parent)?;
}
fs::write(&output_path, file_data)
.with_context(|| format!("Failed to write file '{}'", output_path.display()))?;
}
Node::CurrentDirectory => {}
Node::ParentDirectory => {}
}
}
Ok(())
let path = Utf8NativePathBuf::from(format!("{}:", args.file));
let output = args.output.unwrap_or_else(|| Utf8NativePathBuf::from("."));
vfs::cp(vfs::CpArgs {
paths: vec![path, output],
no_decompress: args.no_decompress,
quiet: args.quiet,
})
}

View File

@ -1,8 +1,7 @@
use std::{
collections::{btree_map, BTreeMap},
fs,
io::Write,
path::PathBuf,
io::{Cursor, Write},
time::Instant,
};
@ -15,6 +14,7 @@ use object::{
use rayon::prelude::*;
use rustc_hash::FxHashMap;
use tracing::{info, info_span};
use typed_path::Utf8NativePathBuf;
use crate::{
analysis::{
@ -27,20 +27,25 @@ use crate::{
tracker::Tracker,
},
array_ref_mut,
cmd::dol::{ModuleConfig, ProjectConfig},
obj::{ObjInfo, ObjReloc, ObjRelocKind, ObjSection, ObjSectionKind, ObjSymbol},
cmd::dol::{find_object_base, ModuleConfig, ObjectBase, ProjectConfig},
obj::{
ObjInfo, ObjReloc, ObjRelocKind, ObjSection, ObjSectionKind, ObjSymbol,
SectionIndex as ObjSectionIndex,
},
util::{
config::{is_auto_symbol, read_splits_sections, SectionDef},
dol::process_dol,
elf::{to_obj_reloc_kind, write_elf},
file::{buf_reader, buf_writer, map_file, process_rsp, verify_hash, FileIterator},
file::{buf_writer, process_rsp, verify_hash, FileIterator},
nested::NestedMap,
path::native_path,
rel::{
print_relocations, process_rel, process_rel_header, process_rel_sections, write_rel,
RelHeader, RelReloc, RelSectionHeader, RelWriteInfo, PERMITTED_SECTIONS,
},
IntoCow, ToCow,
},
vfs::open_file,
};
#[derive(FromArgs, PartialEq, Debug)]
@ -63,9 +68,9 @@ enum SubCommand {
/// Views REL file information.
#[argp(subcommand, name = "info")]
pub struct InfoArgs {
#[argp(positional)]
#[argp(positional, from_str_fn(native_path))]
/// REL file
rel_file: PathBuf,
rel_file: Utf8NativePathBuf,
#[argp(switch, short = 'r')]
/// print relocations
relocations: bool,
@ -75,27 +80,27 @@ pub struct InfoArgs {
/// Merges a DOL + REL(s) into an ELF.
#[argp(subcommand, name = "merge")]
pub struct MergeArgs {
#[argp(positional)]
#[argp(positional, from_str_fn(native_path))]
/// DOL file
dol_file: PathBuf,
#[argp(positional)]
dol_file: Utf8NativePathBuf,
#[argp(positional, from_str_fn(native_path))]
/// REL file(s)
rel_files: Vec<PathBuf>,
#[argp(option, short = 'o')]
rel_files: Vec<Utf8NativePathBuf>,
#[argp(option, short = 'o', from_str_fn(native_path))]
/// output ELF
out_file: PathBuf,
out_file: Utf8NativePathBuf,
}
#[derive(FromArgs, PartialEq, Eq, Debug)]
/// Creates RELs from an ELF + PLF(s).
#[argp(subcommand, name = "make")]
pub struct MakeArgs {
#[argp(positional)]
#[argp(positional, from_str_fn(native_path))]
/// input file(s)
files: Vec<PathBuf>,
#[argp(option, short = 'c')]
files: Vec<Utf8NativePathBuf>,
#[argp(option, short = 'c', from_str_fn(native_path))]
/// (optional) project configuration file
config: Option<PathBuf>,
config: Option<Utf8NativePathBuf>,
#[argp(option, short = 'n')]
/// (optional) module names
names: Vec<String>,
@ -162,16 +167,17 @@ fn match_section_index(
// })
}
fn load_rel(module_config: &ModuleConfig) -> Result<RelInfo> {
let file = map_file(&module_config.object)?;
fn load_rel(module_config: &ModuleConfig, object_base: &ObjectBase) -> Result<RelInfo> {
let mut file = object_base.open(&module_config.object)?;
let data = file.map()?;
if let Some(hash_str) = &module_config.hash {
verify_hash(file.as_slice(), hash_str)?;
verify_hash(data, hash_str)?;
}
let mut reader = file.as_reader();
let mut reader = Cursor::new(data);
let header = process_rel_header(&mut reader)?;
let sections = process_rel_sections(&mut reader, &header)?;
let section_defs = if let Some(splits_path) = &module_config.splits {
read_splits_sections(splits_path)?
read_splits_sections(&splits_path.with_encoding())?
} else {
None
};
@ -181,7 +187,7 @@ fn load_rel(module_config: &ModuleConfig) -> Result<RelInfo> {
struct LoadedModule<'a> {
module_id: u32,
file: File<'a>,
path: PathBuf,
path: Utf8NativePathBuf,
}
fn resolve_relocations(
@ -261,15 +267,19 @@ fn make(args: MakeArgs) -> Result<()> {
let mut existing_headers = BTreeMap::<u32, RelInfo>::new();
let mut name_to_module_id = FxHashMap::<String, u32>::default();
if let Some(config_path) = &args.config {
let config: ProjectConfig = serde_yaml::from_reader(&mut buf_reader(config_path)?)?;
let config: ProjectConfig = {
let mut file = open_file(config_path, true)?;
serde_yaml::from_reader(file.as_mut())?
};
let object_base = find_object_base(&config)?;
for module_config in &config.modules {
let module_name = module_config.name();
if !args.names.is_empty() && !args.names.iter().any(|n| n == &module_name) {
if !args.names.is_empty() && !args.names.iter().any(|n| n == module_name) {
continue;
}
let _span = info_span!("module", name = %module_name).entered();
let info = load_rel(module_config).with_context(|| {
format!("While loading REL '{}'", module_config.object.display())
let info = load_rel(module_config, &object_base).with_context(|| {
format!("While loading REL '{}'", object_base.join(&module_config.object))
})?;
name_to_module_id.insert(module_name.to_string(), info.0.module_id);
match existing_headers.entry(info.0.module_id) {
@ -287,9 +297,9 @@ fn make(args: MakeArgs) -> Result<()> {
}
// Load all modules
let files = paths.iter().map(map_file).collect::<Result<Vec<_>>>()?;
let mut files = paths.iter().map(|p| open_file(p, true)).collect::<Result<Vec<_>>>()?;
let modules = files
.par_iter()
.par_iter_mut()
.enumerate()
.zip(&paths)
.map(|((idx, file), path)| {
@ -301,9 +311,9 @@ fn make(args: MakeArgs) -> Result<()> {
.and_then(|n| name_to_module_id.get(n))
.copied()
.unwrap_or(idx as u32);
load_obj(file.as_slice())
load_obj(file.map()?)
.map(|o| LoadedModule { module_id, file: o, path: path.clone() })
.with_context(|| format!("Failed to load '{}'", path.display()))
.with_context(|| format!("Failed to load '{}'", path))
})
.collect::<Result<Vec<_>>>()?;
@ -311,7 +321,7 @@ fn make(args: MakeArgs) -> Result<()> {
let start = Instant::now();
let mut symbol_map = FxHashMap::<&[u8], (u32, SymbolIndex)>::default();
for module_info in modules.iter() {
let _span = info_span!("file", path = %module_info.path.display()).entered();
let _span = info_span!("file", path = %module_info.path).entered();
for symbol in module_info.file.symbols() {
if symbol.scope() == object::SymbolScope::Dynamic {
symbol_map
@ -326,7 +336,7 @@ fn make(args: MakeArgs) -> Result<()> {
let mut relocations = Vec::<Vec<RelReloc>>::with_capacity(modules.len() - 1);
relocations.resize_with(modules.len() - 1, Vec::new);
for (module_info, relocations) in modules.iter().skip(1).zip(&mut relocations) {
let _span = info_span!("file", path = %module_info.path.display()).entered();
let _span = info_span!("file", path = %module_info.path).entered();
resolved += resolve_relocations(
&module_info.file,
&existing_headers,
@ -335,9 +345,7 @@ fn make(args: MakeArgs) -> Result<()> {
&modules,
relocations,
)
.with_context(|| {
format!("While resolving relocations in '{}'", module_info.path.display())
})?;
.with_context(|| format!("While resolving relocations in '{}'", module_info.path))?;
}
if !args.quiet {
@ -353,7 +361,7 @@ fn make(args: MakeArgs) -> Result<()> {
// Write RELs
let start = Instant::now();
for (module_info, relocations) in modules.iter().skip(1).zip(relocations) {
let _span = info_span!("file", path = %module_info.path.display()).entered();
let _span = info_span!("file", path = %module_info.path).entered();
let mut info = RelWriteInfo {
module_id: module_info.module_id,
version: 3,
@ -384,7 +392,7 @@ fn make(args: MakeArgs) -> Result<()> {
let rel_path = module_info.path.with_extension("rel");
let mut w = buf_writer(&rel_path)?;
write_rel(&mut w, &info, &module_info.file, relocations)
.with_context(|| format!("Failed to write '{}'", rel_path.display()))?;
.with_context(|| format!("Failed to write '{}'", rel_path))?;
w.flush()?;
}
@ -399,8 +407,8 @@ fn make(args: MakeArgs) -> Result<()> {
}
fn info(args: InfoArgs) -> Result<()> {
let file = map_file(args.rel_file)?;
let (header, mut module_obj) = process_rel(&mut file.as_reader(), "")?;
let mut file = open_file(&args.rel_file, true)?;
let (header, mut module_obj) = process_rel(file.as_mut(), "")?;
let mut state = AnalyzerState::default();
state.detect_functions(&module_obj)?;
@ -458,7 +466,7 @@ fn info(args: InfoArgs) -> Result<()> {
if args.relocations {
println!("\nRelocations:");
println!(" [Source] section:address RelocType -> [Target] module:section:address");
print_relocations(&mut file.as_reader(), &header)?;
print_relocations(file.as_mut(), &header)?;
}
Ok(())
}
@ -467,11 +475,11 @@ fn info(args: InfoArgs) -> Result<()> {
const fn align32(x: u32) -> u32 { (x + 31) & !31 }
fn merge(args: MergeArgs) -> Result<()> {
log::info!("Loading {}", args.dol_file.display());
log::info!("Loading {}", args.dol_file);
let mut obj = {
let file = map_file(&args.dol_file)?;
let name = args.dol_file.file_stem().map(|s| s.to_string_lossy()).unwrap_or_default();
process_dol(file.as_slice(), name.as_ref())?
let mut file = open_file(&args.dol_file, true)?;
let name = args.dol_file.file_stem().unwrap_or_default();
process_dol(file.map()?, name)?
};
log::info!("Performing signature analysis");
@ -481,10 +489,10 @@ fn merge(args: MergeArgs) -> Result<()> {
let mut processed = 0;
let mut module_map = BTreeMap::<u32, ObjInfo>::new();
for result in FileIterator::new(&args.rel_files)? {
let (path, entry) = result?;
log::info!("Loading {}", path.display());
let name = path.file_stem().map(|s| s.to_string_lossy()).unwrap_or_default();
let (_, obj) = process_rel(&mut entry.as_reader(), name.as_ref())?;
let (path, mut entry) = result?;
log::info!("Loading {}", path);
let name = path.file_stem().unwrap_or_default();
let (_, obj) = process_rel(&mut entry, name)?;
match module_map.entry(obj.module_id) {
btree_map::Entry::Vacant(e) => e.insert(obj),
btree_map::Entry::Occupied(_) => bail!("Duplicate module ID {}", obj.module_id),
@ -493,7 +501,7 @@ fn merge(args: MergeArgs) -> Result<()> {
}
log::info!("Merging {} REL(s)", processed);
let mut section_map: BTreeMap<u32, BTreeMap<u32, u32>> = BTreeMap::new();
let mut section_map: BTreeMap<u32, BTreeMap<ObjSectionIndex, u32>> = BTreeMap::new();
let mut offset = align32(arena_lo + 0x2000);
for module in module_map.values() {
for (mod_section_index, mod_section) in module.sections.iter() {
@ -512,7 +520,7 @@ fn merge(args: MergeArgs) -> Result<()> {
section_known: mod_section.section_known,
splits: mod_section.splits.clone(),
});
section_map.nested_insert(module.module_id, mod_section.elf_index as u32, offset)?;
section_map.nested_insert(module.module_id, mod_section.elf_index, offset)?;
for (_, mod_symbol) in module.symbols.for_section(mod_section_index) {
obj.symbols.add_direct(ObjSymbol {
name: mod_symbol.name.clone(),
@ -536,7 +544,8 @@ fn merge(args: MergeArgs) -> Result<()> {
log::info!("Applying REL relocations");
for module in module_map.values() {
for rel_reloc in &module.unresolved_relocations {
let source_addr = (section_map[&module.module_id][&(rel_reloc.section as u32)]
let source_addr = (section_map[&module.module_id]
[&(rel_reloc.section as ObjSectionIndex)]
+ rel_reloc.address)
& !3;
let target_addr = if rel_reloc.module_id == 0 {
@ -600,7 +609,7 @@ fn merge(args: MergeArgs) -> Result<()> {
tracker.apply(&mut obj, false)?;
// Write ELF
log::info!("Writing {}", args.out_file.display());
log::info!("Writing {}", args.out_file);
fs::write(&args.out_file, write_elf(&obj, false)?)?;
Ok(())
}

View File

@ -1,9 +1,26 @@
use std::path::PathBuf;
use std::io::{BufRead, Seek, Write};
use anyhow::Result;
use anyhow::{bail, ensure, Context, Result};
use argp::FromArgs;
use object::{
elf::{R_PPC_NONE, R_PPC_REL24},
Architecture, Endianness, Object, ObjectKind, ObjectSection, ObjectSymbol, SectionKind,
SymbolIndex, SymbolKind, SymbolSection,
};
use typed_path::{Utf8NativePath, Utf8NativePathBuf};
use crate::util::{file::map_file, rso::process_rso};
use crate::{
util::{
file::buf_writer,
path::native_path,
reader::{Endian, ToWriter},
rso::{
process_rso, symbol_hash, RsoHeader, RsoRelocation, RsoSectionHeader, RsoSymbol,
RSO_SECTION_NAMES,
},
},
vfs::open_file,
};
#[derive(FromArgs, PartialEq, Debug)]
/// Commands for processing RSO files.
@ -17,30 +34,486 @@ pub struct Args {
#[argp(subcommand)]
enum SubCommand {
Info(InfoArgs),
Make(MakeArgs),
}
#[derive(FromArgs, PartialEq, Eq, Debug)]
/// Views RSO file information.
#[argp(subcommand, name = "info")]
pub struct InfoArgs {
#[argp(positional)]
#[argp(positional, from_str_fn(native_path))]
/// RSO file
rso_file: PathBuf,
rso_file: Utf8NativePathBuf,
}
#[derive(FromArgs, PartialEq, Eq, Debug)]
/// Creates an RSO from an ELF.
#[argp(subcommand, name = "make")]
pub struct MakeArgs {
#[argp(positional, arg_name = "ELF File", from_str_fn(native_path))]
/// elf file
input: Utf8NativePathBuf,
#[argp(option, short = 'o', arg_name = "File", from_str_fn(native_path))]
/// output file path
output: Utf8NativePathBuf,
#[argp(option, short = 'm', arg_name = "Name")]
/// module name (or path). Default: input name
module_name: Option<String>,
#[argp(option, short = 'e', arg_name = "File", from_str_fn(native_path))]
/// file containing exported symbol names (newline separated)
export: Option<Utf8NativePathBuf>,
}
pub fn run(args: Args) -> Result<()> {
match args.command {
SubCommand::Info(c_args) => info(c_args),
SubCommand::Make(c_args) => make(c_args),
}
}
fn info(args: InfoArgs) -> Result<()> {
let rso = {
let file = map_file(args.rso_file)?;
let obj = process_rso(&mut file.as_reader())?;
#[allow(clippy::let_and_return)]
obj
let mut file = open_file(&args.rso_file, true)?;
process_rso(file.as_mut())?
};
println!("Read RSO module {}", rso.name);
Ok(())
}
fn make(args: MakeArgs) -> Result<()> {
let mut file = open_file(&args.input, true)?;
let obj_file = object::read::File::parse(file.map()?)?;
match obj_file.architecture() {
Architecture::PowerPc => {}
arch => bail!("Unexpected architecture: {arch:?}"),
};
ensure!(obj_file.endianness() == Endianness::Big, "Expected big endian");
let module_name = match args.module_name {
Some(n) => n,
None => args.input.to_string(),
};
let symbols_to_export = match &args.export {
Some(export_file_path) => {
let export_file_reader = open_file(export_file_path, true)?;
export_file_reader.lines().map_while(Result::ok).collect()
}
None => vec![],
};
match obj_file.kind() {
ObjectKind::Executable => {
make_sel(obj_file, &args.output, &module_name, symbols_to_export)?
}
ObjectKind::Relocatable => {
make_rso(obj_file, &args.output, &module_name, symbols_to_export)?
}
kind => bail!("Unexpected ELF type: {kind:?}"),
}
Ok(())
}
fn make_sel(
_file: object::File,
_output: &Utf8NativePath,
_module_name: &str,
_symbols_to_export: Vec<String>,
) -> Result<()> {
bail!("Creating SEL files is not supported yet.");
}
fn make_rso(
file: object::File,
output: &Utf8NativePath,
module_name: &str,
symbols_to_export: Vec<String>,
) -> Result<()> {
let mut out = buf_writer(output)?;
let try_populate_symbol_index_and_offset =
|name: &str, index: &mut u8, offset: &mut u32| -> Result<()> {
let Some(sym) = file.symbol_by_name(name) else {
return Ok(());
};
let si = sym
.section_index()
.with_context(|| format!("Failed to find symbol `{}` section index", name))?;
let addr = sym.address();
*index = si.0 as u8;
*offset = addr as u32;
Ok(())
};
let pad_to_alignment =
|out: &mut std::io::BufWriter<std::fs::File>, alignment: u64| -> Result<()> {
if alignment == 0 {
return Ok(());
}
const ZERO_BUF: [u8; 32] = [0u8; 32];
let pos = out.stream_position()?;
let mut count = (!(alignment - 1) & ((alignment + pos) - 1)) - pos;
while count > 0 {
let slice_size = std::cmp::min(ZERO_BUF.len(), count as usize);
out.write_all(&ZERO_BUF[0..slice_size])?;
count -= slice_size as u64;
}
Ok(())
};
let mut header = RsoHeader::new();
try_populate_symbol_index_and_offset(
"_prolog",
&mut header.prolog_section,
&mut header.prolog_offset,
)?;
try_populate_symbol_index_and_offset(
"_epilog",
&mut header.epilog_section,
&mut header.epilog_offset,
)?;
try_populate_symbol_index_and_offset(
"_unresolved",
&mut header.unresolved_section,
&mut header.unresolved_offset,
)?;
header.to_writer(&mut out, Endian::Big)?;
header.section_info_offset = out.stream_position()? as u32;
{
// Write Sections Info Table (Blank)
let blank_section = RsoSectionHeader::default();
// Include ELF null section
for _ in 0..file.sections().count() + 1 {
header.num_sections += 1;
blank_section.to_writer(&mut out, Endian::Big)?;
}
}
let mut rso_sections: Vec<RsoSectionHeader> =
vec![RsoSectionHeader::default() /* ELF null section */];
for section in file.sections() {
let is_valid_section =
section.name().is_ok_and(|n| RSO_SECTION_NAMES.iter().any(|&s| s == n));
let section_size = section.size();
if !is_valid_section || section_size == 0 {
rso_sections.push(RsoSectionHeader::default());
continue;
}
if section.kind() == SectionKind::UninitializedData {
header.bss_size += section_size as u32;
rso_sections.push(RsoSectionHeader { offset_and_flags: 0, size: section_size as u32 });
continue;
}
pad_to_alignment(&mut out, section.align())?;
let section_offset_in_file = out.stream_position()?;
let section_data = section.data()?;
out.write_all(section_data)?;
rso_sections.push(RsoSectionHeader {
offset_and_flags: section_offset_in_file as u32,
size: section_size as u32,
});
}
pad_to_alignment(&mut out, 4)?;
header.name_offset = out.stream_position()? as u32;
// Rewind and write the correct section info table
out.seek(std::io::SeekFrom::Start(header.section_info_offset as u64))?;
for section in &rso_sections {
section.to_writer(&mut out, Endian::Big)?;
}
// Write the module name
out.seek(std::io::SeekFrom::Start(header.name_offset as u64))?;
let module_name = module_name.as_bytes();
out.write_all(module_name)?;
header.name_size = module_name.len() as u32;
// Accumulate exported and imported symbol
let mut import_symbol_table: Vec<RsoSymbol> = vec![];
let mut export_symbol_table: Vec<RsoSymbol> = vec![];
for symbol in file.symbols() {
let sym_binding = match symbol.flags() {
object::SymbolFlags::Elf { st_info, st_other: _ } => st_info >> 4,
flag => bail!("Unknown symbol flag found `{:?}`", flag),
};
let symbol_name = match symbol.name() {
Ok(n) => {
if n.is_empty() {
continue;
}
n
}
Err(_) => continue,
};
// In the [`RsoSymbol::name_offset`] field we would store the symbol index temp
match symbol.section_index() {
Some(section_index)
if sym_binding != object::elf::STB_LOCAL && section_index.0 != 0 =>
{
// Symbol to export
if !symbols_to_export.iter().any(|s| s == symbol_name) {
continue;
}
let hash = symbol_hash(symbol_name);
export_symbol_table.push(RsoSymbol {
name_offset: symbol.index().0 as u32,
offset: symbol.address() as u32,
section_index: section_index.0 as u32,
hash: Some(hash),
});
}
None => {
if matches!(symbol.kind(), SymbolKind::File) {
continue;
}
if symbol.section() == SymbolSection::Absolute {
if !symbols_to_export.iter().any(|s| s == symbol_name) {
continue;
}
// Special Symbols
let hash = symbol_hash(symbol_name);
export_symbol_table.push(RsoSymbol {
name_offset: symbol.index().0 as u32,
offset: symbol.address() as u32,
section_index: 0xFFF1_u32,
hash: Some(hash),
});
continue;
}
// Symbol to import
import_symbol_table.push(RsoSymbol {
name_offset: symbol.index().0 as u32,
offset: symbol.address() as u32,
section_index: 0, // Relocation offset
hash: None,
});
}
_ => continue,
}
}
// Accumulate relocations
let mut imported_relocations: Vec<RsoRelocation> = vec![];
let mut exported_relocations: Vec<RsoRelocation> = vec![];
for section in file.sections() {
let is_valid_section =
section.name().is_ok_and(|n| RSO_SECTION_NAMES.iter().any(|&s| s == n));
if !is_valid_section {
continue;
}
let relocation_section_idx = section.index().0 as u32;
let relocation_section_offset =
rso_sections[relocation_section_idx as usize].offset_and_flags;
for (reloc_addr, reloc) in section.relocations() {
let reloc_target_symbol_idx = match reloc.target() {
object::RelocationTarget::Symbol(t) => t,
_ => continue,
};
let Ok(reloc_target_symbol) = file.symbol_by_index(reloc_target_symbol_idx) else {
bail!(
"Failed to find relocation `{:08X}` symbol ({})",
reloc_addr,
reloc_target_symbol_idx.0
);
};
let reloc_type = match reloc.flags() {
object::RelocationFlags::Elf { r_type } => r_type,
_ => continue,
};
if reloc_type == R_PPC_NONE {
continue;
}
match reloc_target_symbol.section_index() {
None => {
// Imported symbol relocation
// Get the symbol index inside the import symbol table
let symbol_table_idx = match import_symbol_table
.iter()
.position(|s| s.name_offset == reloc_target_symbol_idx.0 as u32)
{
Some(idx) => idx,
// We should always find the symbol. If not, it means a logic error in the symbol accumulator loop
// panic?
None => {
bail!("Failed to find imported symbol in the accumulated symbol table.")
}
};
let id_and_type = ((symbol_table_idx as u32) << 8) | (reloc_type & 0xFF);
imported_relocations.push(RsoRelocation {
// Convert the relocation offset from being section relative to file relative
offset: relocation_section_offset + reloc_addr as u32,
id_and_type,
target_offset: 0,
});
}
Some(reloc_symbol_section_idx) => {
// Exported symbol relocation
let id_and_type =
((reloc_symbol_section_idx.0 as u32) << 8) | (reloc_type & 0xFF);
exported_relocations.push(RsoRelocation {
// Convert the relocation offset from being section relative to file relative
offset: relocation_section_offset + reloc_addr as u32,
id_and_type,
target_offset: reloc.addend() as u32 + reloc_target_symbol.address() as u32,
});
}
}
// Apply relocation with the `_unresolved` as the symbol, if the module export the function
if reloc_type == R_PPC_REL24
&& header.unresolved_offset != 0
&& header.unresolved_section == relocation_section_idx as u8
{
let target_section = file
.section_by_index(object::SectionIndex(relocation_section_idx as usize))
.unwrap();
let target_section_data = target_section.data().unwrap();
// Copy instruction
let mut intruction_buff = [0u8; 4];
intruction_buff.copy_from_slice(
&target_section_data[(reloc_addr as usize)..(reloc_addr + 4) as usize],
);
let target_instruction = u32::from_be_bytes(intruction_buff);
let off_diff = header.unresolved_offset as i64 - reloc_addr as i64;
let replacement_instruction =
(off_diff as u32 & 0x3fffffcu32) | (target_instruction & 0xfc000003u32);
let intruction_buff = replacement_instruction.to_be_bytes();
let relocation_file_offset = relocation_section_offset as u64 + reloc_addr;
let current_stream_pos = out.stream_position()?;
out.seek(std::io::SeekFrom::Start(relocation_file_offset))?;
out.write_all(&intruction_buff)?;
out.seek(std::io::SeekFrom::Start(current_stream_pos))?;
}
}
}
// Sort imported relocation, by symbol index
imported_relocations.sort_by(|lhs, rhs| {
let lhs_symbol_idx = lhs.id();
let rhs_symbol_idx = rhs.id();
rhs_symbol_idx.cmp(&lhs_symbol_idx)
});
// Sort Export Symbol by Hash
export_symbol_table.sort_by(|lhs, rhs| rhs.hash.unwrap().cmp(&lhs.hash.unwrap()));
{
// Write Export Symbol Table
pad_to_alignment(&mut out, 4)?;
header.export_table_offset = out.stream_position()? as u32;
header.export_table_size = (export_symbol_table.len() * 16) as u32;
let mut export_symbol_name_table: Vec<u8> = vec![];
for export_symbol in &mut export_symbol_table {
let export_elf_symbol =
file.symbol_by_index(SymbolIndex(export_symbol.name_offset as usize)).unwrap();
let export_elf_symbol_name = export_elf_symbol.name().unwrap();
export_symbol.name_offset = export_symbol_name_table.len() as u32;
export_symbol.to_writer(&mut out, Endian::Big)?;
export_symbol_name_table.extend_from_slice(export_elf_symbol_name.as_bytes());
export_symbol_name_table.push(0u8); // '\0'
}
// Write Export Symbol Name Table
pad_to_alignment(&mut out, 4)?;
header.export_table_name_offset = out.stream_position()? as u32;
out.write_all(&export_symbol_name_table)?;
}
{
// Write Imported Symbol Relocation
pad_to_alignment(&mut out, 4)?;
header.external_rel_offset = out.stream_position()? as u32;
header.external_rel_size = (imported_relocations.len() * 12) as u32;
for reloc in &imported_relocations {
reloc.to_writer(&mut out, Endian::Big)?;
}
}
{
pad_to_alignment(&mut out, 4)?;
header.import_table_offset = out.stream_position()? as u32;
header.import_table_size = (import_symbol_table.len() * 12) as u32;
let mut import_symbol_name_table: Vec<u8> = vec![];
for (import_symbol_idx, import_symbol) in import_symbol_table.iter_mut().enumerate() {
let import_elf_symbol_idx = import_symbol.name_offset as usize;
let import_elf_symbol =
file.symbol_by_index(SymbolIndex(import_elf_symbol_idx)).unwrap();
let import_elf_symbol_name = import_elf_symbol.name().unwrap();
import_symbol.name_offset = import_symbol_name_table.len() as u32;
// Gather the index of the first relocation that utilize this symbol
let first_relocation_offset = imported_relocations
.iter()
.position(|r| r.id() == import_symbol_idx as u32)
.map(|idx| idx * 12)
.unwrap_or(usize::MAX) as u32;
import_symbol.section_index = first_relocation_offset;
import_symbol.to_writer(&mut out, Endian::Big)?;
import_symbol_name_table.extend_from_slice(import_elf_symbol_name.as_bytes());
import_symbol_name_table.push(0u8); // '\0'
}
// Write Export Symbol Name Table
pad_to_alignment(&mut out, 4)?;
header.import_table_name_offset = out.stream_position()? as u32;
out.write_all(&import_symbol_name_table)?;
}
{
// Write Internal Relocation Table
pad_to_alignment(&mut out, 4)?;
header.internal_rel_offset = out.stream_position()? as u32;
header.internal_rel_size = (exported_relocations.len() * 12) as u32;
for reloc in &exported_relocations {
reloc.to_writer(&mut out, Endian::Big)?;
}
}
pad_to_alignment(&mut out, 32)?;
out.seek(std::io::SeekFrom::Start(0))?;
header.to_writer(&mut out, Endian::Big)?;
Ok(())
}

View File

@ -1,16 +1,21 @@
use std::{
fs::File,
io::{stdout, BufRead, BufReader, Read, Write},
path::{Path, PathBuf},
io::{stdout, BufRead, Read, Write},
};
use anyhow::{anyhow, bail, Context, Result};
use argp::FromArgs;
use owo_colors::{OwoColorize, Stream};
use path_slash::PathExt;
use sha1::{Digest, Sha1};
use typed_path::{Utf8NativePath, Utf8NativePathBuf};
use crate::util::file::{buf_writer, open_file, process_rsp, touch};
use crate::{
util::{
file::{buf_writer, process_rsp, touch},
path::native_path,
},
vfs::open_file,
};
#[derive(FromArgs, PartialEq, Eq, Debug)]
/// Print or check SHA1 (160-bit) checksums.
@ -19,13 +24,13 @@ pub struct Args {
#[argp(switch, short = 'c')]
/// check SHA sums against given list
check: bool,
#[argp(positional)]
#[argp(positional, from_str_fn(native_path))]
/// path to input file(s)
files: Vec<PathBuf>,
#[argp(option, short = 'o')]
files: Vec<Utf8NativePathBuf>,
#[argp(option, short = 'o', from_str_fn(native_path))]
/// (check) touch output file on successful check
/// (hash) write hash(es) to output file
output: Option<PathBuf>,
output: Option<Utf8NativePathBuf>,
#[argp(switch, short = 'q')]
/// only print failures and a summary
quiet: bool,
@ -36,25 +41,25 @@ const DEFAULT_BUF_SIZE: usize = 8192;
pub fn run(args: Args) -> Result<()> {
if args.check {
for path in process_rsp(&args.files)? {
let file = open_file(&path)?;
check(&args, &mut BufReader::new(file))?;
let mut file = open_file(&path, false)?;
check(&args, file.as_mut())?;
}
if let Some(out_path) = &args.output {
touch(out_path)
.with_context(|| format!("Failed to touch output file '{}'", out_path.display()))?;
.with_context(|| format!("Failed to touch output file '{}'", out_path))?;
}
} else {
let mut w: Box<dyn Write> =
if let Some(out_path) = &args.output {
Box::new(buf_writer(out_path).with_context(|| {
format!("Failed to open output file '{}'", out_path.display())
})?)
} else {
Box::new(stdout())
};
let mut w: Box<dyn Write> = if let Some(out_path) = &args.output {
Box::new(
buf_writer(out_path)
.with_context(|| format!("Failed to open output file '{}'", out_path))?,
)
} else {
Box::new(stdout())
};
for path in process_rsp(&args.files)? {
let mut file = open_file(&path)?;
hash(w.as_mut(), &mut file, &path)?
let mut file = open_file(&path, false)?;
hash(w.as_mut(), file.as_mut(), &path)?
}
}
Ok(())
@ -111,7 +116,7 @@ where R: BufRead + ?Sized {
Ok(())
}
fn hash<R, W>(w: &mut W, reader: &mut R, path: &Path) -> Result<()>
fn hash<R, W>(w: &mut W, reader: &mut R, path: &Utf8NativePath) -> Result<()>
where
R: Read + ?Sized,
W: Write + ?Sized,
@ -120,7 +125,7 @@ where
let mut hash_buf = [0u8; 40];
let hash_str = base16ct::lower::encode_str(&hash, &mut hash_buf)
.map_err(|e| anyhow!("Failed to encode hash: {e}"))?;
writeln!(w, "{} {}", hash_str, path.to_slash_lossy())?;
writeln!(w, "{} {}", hash_str, path.with_unix_encoding())?;
Ok(())
}

View File

@ -1,13 +1,9 @@
use std::{borrow::Cow, fs, fs::DirBuilder, path::PathBuf};
use anyhow::{anyhow, Context, Result};
use anyhow::Result;
use argp::FromArgs;
use itertools::Itertools;
use typed_path::Utf8NativePathBuf;
use crate::util::{
file::{decompress_if_needed, map_file},
u8_arc::{U8Node, U8View},
};
use super::vfs;
use crate::util::path::native_path;
#[derive(FromArgs, PartialEq, Debug)]
/// Commands for processing U8 (arc) files.
@ -28,23 +24,29 @@ enum SubCommand {
/// Views U8 (arc) file information.
#[argp(subcommand, name = "list")]
pub struct ListArgs {
#[argp(positional)]
#[argp(positional, from_str_fn(native_path))]
/// U8 (arc) file
file: PathBuf,
file: Utf8NativePathBuf,
#[argp(switch, short = 's')]
/// Only print filenames.
short: bool,
}
#[derive(FromArgs, PartialEq, Eq, Debug)]
/// Extracts U8 (arc) file contents.
#[argp(subcommand, name = "extract")]
pub struct ExtractArgs {
#[argp(positional)]
#[argp(positional, from_str_fn(native_path))]
/// U8 (arc) file
file: PathBuf,
#[argp(option, short = 'o')]
file: Utf8NativePathBuf,
#[argp(option, short = 'o', from_str_fn(native_path))]
/// output directory
output: Option<PathBuf>,
output: Option<Utf8NativePathBuf>,
#[argp(switch)]
/// Do not decompress files when copying.
no_decompress: bool,
#[argp(switch, short = 'q')]
/// quiet output
/// Quiet output. Don't print anything except errors.
quiet: bool,
}
@ -56,66 +58,16 @@ pub fn run(args: Args) -> Result<()> {
}
fn list(args: ListArgs) -> Result<()> {
let file = map_file(&args.file)?;
let view = U8View::new(file.as_slice())
.map_err(|e| anyhow!("Failed to open U8 file '{}': {}", args.file.display(), e))?;
visit_files(&view, |_, node, path| {
println!("{}: {} bytes, offset {:#X}", path, node.length(), node.offset());
Ok(())
})
let path = Utf8NativePathBuf::from(format!("{}:", args.file));
vfs::ls(vfs::LsArgs { path, short: args.short, recursive: true })
}
fn extract(args: ExtractArgs) -> Result<()> {
let file = map_file(&args.file)?;
let view = U8View::new(file.as_slice())
.map_err(|e| anyhow!("Failed to open U8 file '{}': {}", args.file.display(), e))?;
visit_files(&view, |_, node, path| {
let offset = node.offset();
let size = node.length();
let file_data = decompress_if_needed(
&file.as_slice()[offset as usize..offset as usize + size as usize],
)?;
let output_path = args
.output
.as_ref()
.map(|p| p.join(&path))
.unwrap_or_else(|| PathBuf::from(path.clone()));
if !args.quiet {
println!("Extracting {} to {} ({} bytes)", path, output_path.display(), size);
}
if let Some(parent) = output_path.parent() {
DirBuilder::new().recursive(true).create(parent)?;
}
fs::write(&output_path, file_data)
.with_context(|| format!("Failed to write file '{}'", output_path.display()))?;
Ok(())
let path = Utf8NativePathBuf::from(format!("{}:", args.file));
let output = args.output.unwrap_or_else(|| Utf8NativePathBuf::from("."));
vfs::cp(vfs::CpArgs {
paths: vec![path, output],
no_decompress: args.no_decompress,
quiet: args.quiet,
})
}
fn visit_files(
view: &U8View,
mut visitor: impl FnMut(usize, &U8Node, String) -> Result<()>,
) -> Result<()> {
let mut path_segments = Vec::<(Cow<str>, usize)>::new();
for (idx, node, name) in view.iter() {
// Remove ended path segments
let mut new_size = 0;
for (_, end) in path_segments.iter() {
if *end == idx {
break;
}
new_size += 1;
}
path_segments.truncate(new_size);
// Add the new path segment
let end = if node.is_dir() { node.length() as usize } else { idx + 1 };
path_segments.push((name.map_err(|e| anyhow!("{}", e))?, end));
let path = path_segments.iter().map(|(name, _)| name.as_ref()).join("/");
if !node.is_dir() {
visitor(idx, node, path)?;
}
}
Ok(())
}

286
src/cmd/vfs.rs Normal file
View File

@ -0,0 +1,286 @@
use std::{fs, fs::File, io::Write};
use anyhow::{anyhow, bail, Context};
use argp::FromArgs;
use size::Size;
use typed_path::{Utf8NativePath, Utf8NativePathBuf, Utf8UnixPath};
use crate::{
util::{file::buf_copy, path::native_path},
vfs::{
decompress_file, detect, open_path, FileFormat, OpenResult, Vfs, VfsFile, VfsFileType,
VfsMetadata,
},
};
#[derive(FromArgs, PartialEq, Debug)]
/// Commands for interacting with discs and containers.
#[argp(subcommand, name = "vfs")]
pub struct Args {
#[argp(subcommand)]
command: SubCommand,
}
#[derive(FromArgs, PartialEq, Debug)]
#[argp(subcommand)]
enum SubCommand {
Ls(LsArgs),
Cp(CpArgs),
}
#[derive(FromArgs, PartialEq, Eq, Debug)]
/// List files in a directory or container.
#[argp(subcommand, name = "ls")]
pub struct LsArgs {
#[argp(positional, from_str_fn(native_path))]
/// Directory or container path.
pub path: Utf8NativePathBuf,
#[argp(switch, short = 's')]
/// Only print filenames.
pub short: bool,
#[argp(switch, short = 'r')]
/// Recursively list files in directories.
pub recursive: bool,
}
#[derive(FromArgs, PartialEq, Eq, Debug)]
/// Copy files from a container.
#[argp(subcommand, name = "cp")]
pub struct CpArgs {
#[argp(positional, from_str_fn(native_path))]
/// Source path(s) and destination path.
pub paths: Vec<Utf8NativePathBuf>,
#[argp(switch)]
/// Do not decompress files when copying.
pub no_decompress: bool,
#[argp(switch, short = 'q')]
/// Quiet output. Don't print anything except errors.
pub quiet: bool,
}
pub fn run(args: Args) -> anyhow::Result<()> {
match args.command {
SubCommand::Ls(args) => ls(args),
SubCommand::Cp(args) => cp(args),
}
}
const SEPARATOR: &str = " | ";
type Columns<const N: usize> = [String; N];
fn column_widths<const N: usize>(entries: &[Columns<N>]) -> [usize; N] {
let mut widths = [0usize; N];
for text in entries {
for (i, column) in text.iter().enumerate() {
widths[i] = widths[i].max(column.len());
}
}
widths
}
fn file_info(
filename: &str,
file: &mut dyn VfsFile,
metadata: &VfsMetadata,
) -> anyhow::Result<Columns<5>> {
let format =
detect(file).with_context(|| format!("Failed to detect file format for {}", filename))?;
let mut info: Columns<5> = [
Size::from_bytes(metadata.len).to_string(),
filename.to_string(),
format.to_string(),
String::new(),
String::new(),
];
if let FileFormat::Compressed(kind) = format {
let mut decompressed = decompress_file(file, kind)?;
let metadata = decompressed
.metadata()
.with_context(|| format!("Failed to fetch metadata for {}", filename))?;
let format = detect(decompressed.as_mut())
.with_context(|| format!("Failed to detect file format for {}", filename))?;
info[3] = format!("Decompressed: {}", Size::from_bytes(metadata.len));
info[4] = format.to_string();
}
Ok(info)
}
pub fn ls(args: LsArgs) -> anyhow::Result<()> {
let mut files = Vec::new();
match open_path(&args.path, false)? {
OpenResult::File(mut file, path) => {
let filename = path.file_name().ok_or_else(|| anyhow!("Path has no filename"))?;
if args.short {
println!("{}", filename);
} else {
let metadata = file
.metadata()
.with_context(|| format!("Failed to fetch metadata for {}", path))?;
files.push(file_info(filename, file.as_mut(), &metadata)?);
}
}
OpenResult::Directory(mut fs, path) => {
ls_directory(fs.as_mut(), &path, Utf8UnixPath::new(""), &args, &mut files)?;
}
}
if !args.short {
let widths = column_widths(&files);
for entry in files {
let mut written = 0;
for (i, column) in entry.iter().enumerate() {
if widths[i] > 0 {
if written > 0 {
print!("{}", SEPARATOR);
}
written += 1;
print!("{:width$}", column, width = widths[i]);
}
}
println!();
}
}
Ok(())
}
fn ls_directory(
fs: &mut dyn Vfs,
path: &Utf8UnixPath,
base_filename: &Utf8UnixPath,
args: &LsArgs,
files: &mut Vec<Columns<5>>,
) -> anyhow::Result<()> {
let entries = fs.read_dir(path)?;
files.reserve(entries.len());
for filename in entries {
let entry_path = path.join(&filename);
let display_path = base_filename.join(&filename);
let metadata = fs
.metadata(&entry_path)
.with_context(|| format!("Failed to fetch metadata for {}", entry_path))?;
match metadata.file_type {
VfsFileType::File => {
let mut file = fs
.open(&entry_path)
.with_context(|| format!("Failed to open file {}", entry_path))?;
if args.short {
println!("{}", display_path);
} else {
files.push(file_info(display_path.as_str(), file.as_mut(), &metadata)?);
}
}
VfsFileType::Directory => {
if args.short {
println!("{}/", display_path);
} else {
files.push([
" ".to_string(),
format!("{}/", display_path),
"Directory".to_string(),
String::new(),
String::new(),
]);
}
if args.recursive {
ls_directory(fs, &entry_path, &display_path, args, files)?;
}
}
}
}
Ok(())
}
pub fn cp(mut args: CpArgs) -> anyhow::Result<()> {
if args.paths.len() < 2 {
bail!("Both source and destination paths must be provided");
}
let dest = args.paths.pop().unwrap();
let dest_is_dir = args.paths.len() > 1 || fs::metadata(&dest).ok().is_some_and(|m| m.is_dir());
let auto_decompress = !args.no_decompress;
for path in args.paths {
match open_path(&path, auto_decompress)? {
OpenResult::File(file, path) => {
let dest = if dest_is_dir {
fs::create_dir_all(&dest)
.with_context(|| format!("Failed to create directory {}", dest))?;
let filename =
path.file_name().ok_or_else(|| anyhow!("Path has no filename"))?;
dest.join(filename)
} else {
dest.clone()
};
cp_file(file, &path, &dest, auto_decompress, args.quiet)?;
}
OpenResult::Directory(mut fs, path) => {
cp_recursive(fs.as_mut(), &path, &dest, auto_decompress, args.quiet)?;
}
}
}
Ok(())
}
fn cp_file(
mut file: Box<dyn VfsFile>,
path: &Utf8UnixPath,
dest: &Utf8NativePath,
auto_decompress: bool,
quiet: bool,
) -> anyhow::Result<()> {
let mut compression = None;
if let FileFormat::Compressed(kind) = detect(file.as_mut())? {
if auto_decompress {
file = decompress_file(file.as_mut(), kind)
.with_context(|| format!("Failed to decompress file {}", dest))?;
compression = Some(kind);
}
}
let metadata =
file.metadata().with_context(|| format!("Failed to fetch metadata for {}", dest))?;
if !quiet {
if let Some(kind) = compression {
println!(
"{} -> {} ({}) [Decompressed {}]",
path,
dest,
Size::from_bytes(metadata.len),
kind
);
} else {
println!("{} -> {} ({})", path, dest, Size::from_bytes(metadata.len));
}
}
let mut dest_file =
File::create(dest).with_context(|| format!("Failed to create file {}", dest))?;
buf_copy(file.as_mut(), &mut dest_file)
.with_context(|| format!("Failed to copy file {}", dest))?;
dest_file.flush().with_context(|| format!("Failed to flush file {}", dest))?;
Ok(())
}
fn cp_recursive(
fs: &mut dyn Vfs,
path: &Utf8UnixPath,
dest: &Utf8NativePath,
auto_decompress: bool,
quiet: bool,
) -> anyhow::Result<()> {
fs::create_dir_all(dest).with_context(|| format!("Failed to create directory {}", dest))?;
let entries = fs.read_dir(path)?;
for filename in entries {
let entry_path = path.join(&filename);
let metadata = fs
.metadata(&entry_path)
.with_context(|| format!("Failed to fetch metadata for {}", entry_path))?;
match metadata.file_type {
VfsFileType::File => {
let file = fs
.open(&entry_path)
.with_context(|| format!("Failed to open file {}", entry_path))?;
cp_file(file, &entry_path, &dest.join(filename), auto_decompress, quiet)?;
}
VfsFileType::Directory => {
cp_recursive(fs, &entry_path, &dest.join(filename), auto_decompress, quiet)?;
}
}
}
Ok(())
}

View File

@ -1,12 +1,17 @@
use std::{fs, path::PathBuf};
use std::fs;
use anyhow::{Context, Result};
use argp::FromArgs;
use typed_path::Utf8NativePathBuf;
use crate::util::{
file::{map_file_basic, process_rsp},
ncompress::{compress_yay0, decompress_yay0},
IntoCow, ToCow,
use crate::{
util::{
file::process_rsp,
ncompress::{compress_yay0, decompress_yay0},
path::native_path,
IntoCow, ToCow,
},
vfs::open_file,
};
#[derive(FromArgs, PartialEq, Debug)]
@ -28,26 +33,26 @@ enum SubCommand {
/// Compresses files using YAY0.
#[argp(subcommand, name = "compress")]
pub struct CompressArgs {
#[argp(positional)]
#[argp(positional, from_str_fn(native_path))]
/// Files to compress
files: Vec<PathBuf>,
#[argp(option, short = 'o')]
files: Vec<Utf8NativePathBuf>,
#[argp(option, short = 'o', from_str_fn(native_path))]
/// Output file (or directory, if multiple files are specified).
/// If not specified, compresses in-place.
output: Option<PathBuf>,
output: Option<Utf8NativePathBuf>,
}
#[derive(FromArgs, PartialEq, Eq, Debug)]
/// Decompresses YAY0-compressed files.
#[argp(subcommand, name = "decompress")]
pub struct DecompressArgs {
#[argp(positional)]
#[argp(positional, from_str_fn(native_path))]
/// YAY0-compressed files
files: Vec<PathBuf>,
#[argp(option, short = 'o')]
files: Vec<Utf8NativePathBuf>,
#[argp(option, short = 'o', from_str_fn(native_path))]
/// Output file (or directory, if multiple files are specified).
/// If not specified, decompresses in-place.
output: Option<PathBuf>,
output: Option<Utf8NativePathBuf>,
}
pub fn run(args: Args) -> Result<()> {
@ -62,8 +67,8 @@ fn compress(args: CompressArgs) -> Result<()> {
let single_file = files.len() == 1;
for path in files {
let data = {
let file = map_file_basic(&path)?;
compress_yay0(file.as_slice())
let mut file = open_file(&path, false)?;
compress_yay0(file.map()?)
};
let out_path = if let Some(output) = &args.output {
if single_file {
@ -75,7 +80,7 @@ fn compress(args: CompressArgs) -> Result<()> {
path.as_path().to_cow()
};
fs::write(out_path.as_ref(), data)
.with_context(|| format!("Failed to write '{}'", out_path.display()))?;
.with_context(|| format!("Failed to write '{}'", out_path))?;
}
Ok(())
}
@ -85,9 +90,9 @@ fn decompress(args: DecompressArgs) -> Result<()> {
let single_file = files.len() == 1;
for path in files {
let data = {
let file = map_file_basic(&path)?;
decompress_yay0(file.as_slice())
.with_context(|| format!("Failed to decompress '{}' using Yay0", path.display()))?
let mut file = open_file(&path, true)?;
decompress_yay0(file.map()?)
.with_context(|| format!("Failed to decompress '{}' using Yay0", path))?
};
let out_path = if let Some(output) = &args.output {
if single_file {
@ -99,7 +104,7 @@ fn decompress(args: DecompressArgs) -> Result<()> {
path.as_path().to_cow()
};
fs::write(out_path.as_ref(), data)
.with_context(|| format!("Failed to write '{}'", out_path.display()))?;
.with_context(|| format!("Failed to write '{}'", out_path))?;
}
Ok(())
}

View File

@ -1,12 +1,17 @@
use std::{fs, path::PathBuf};
use std::fs;
use anyhow::{Context, Result};
use argp::FromArgs;
use typed_path::Utf8NativePathBuf;
use crate::util::{
file::{map_file_basic, process_rsp},
ncompress::{compress_yaz0, decompress_yaz0},
IntoCow, ToCow,
use crate::{
util::{
file::process_rsp,
ncompress::{compress_yaz0, decompress_yaz0},
path::native_path,
IntoCow, ToCow,
},
vfs::open_file,
};
#[derive(FromArgs, PartialEq, Debug)]
@ -28,26 +33,26 @@ enum SubCommand {
/// Compresses files using YAZ0.
#[argp(subcommand, name = "compress")]
pub struct CompressArgs {
#[argp(positional)]
#[argp(positional, from_str_fn(native_path))]
/// Files to compress
files: Vec<PathBuf>,
#[argp(option, short = 'o')]
files: Vec<Utf8NativePathBuf>,
#[argp(option, short = 'o', from_str_fn(native_path))]
/// Output file (or directory, if multiple files are specified).
/// If not specified, compresses in-place.
output: Option<PathBuf>,
output: Option<Utf8NativePathBuf>,
}
#[derive(FromArgs, PartialEq, Eq, Debug)]
/// Decompresses YAZ0-compressed files.
#[argp(subcommand, name = "decompress")]
pub struct DecompressArgs {
#[argp(positional)]
#[argp(positional, from_str_fn(native_path))]
/// YAZ0-compressed files
files: Vec<PathBuf>,
#[argp(option, short = 'o')]
files: Vec<Utf8NativePathBuf>,
#[argp(option, short = 'o', from_str_fn(native_path))]
/// Output file (or directory, if multiple files are specified).
/// If not specified, decompresses in-place.
output: Option<PathBuf>,
output: Option<Utf8NativePathBuf>,
}
pub fn run(args: Args) -> Result<()> {
@ -62,8 +67,8 @@ fn compress(args: CompressArgs) -> Result<()> {
let single_file = files.len() == 1;
for path in files {
let data = {
let file = map_file_basic(&path)?;
compress_yaz0(file.as_slice())
let mut file = open_file(&path, false)?;
compress_yaz0(file.map()?)
};
let out_path = if let Some(output) = &args.output {
if single_file {
@ -75,7 +80,7 @@ fn compress(args: CompressArgs) -> Result<()> {
path.as_path().to_cow()
};
fs::write(out_path.as_ref(), data)
.with_context(|| format!("Failed to write '{}'", out_path.display()))?;
.with_context(|| format!("Failed to write '{}'", out_path))?;
}
Ok(())
}
@ -85,9 +90,9 @@ fn decompress(args: DecompressArgs) -> Result<()> {
let single_file = files.len() == 1;
for path in files {
let data = {
let file = map_file_basic(&path)?;
decompress_yaz0(file.as_slice())
.with_context(|| format!("Failed to decompress '{}' using Yaz0", path.display()))?
let mut file = open_file(&path, false)?;
decompress_yaz0(file.map()?)
.with_context(|| format!("Failed to decompress '{}' using Yaz0", path))?
};
let out_path = if let Some(output) = &args.output {
if single_file {
@ -99,7 +104,7 @@ fn decompress(args: DecompressArgs) -> Result<()> {
path.as_path().to_cow()
};
fs::write(out_path.as_ref(), data)
.with_context(|| format!("Failed to write '{}'", out_path.display()))?;
.with_context(|| format!("Failed to write '{}'", out_path))?;
}
Ok(())
}

View File

@ -1,3 +1,4 @@
#![deny(unused_crate_dependencies)]
use std::{env, ffi::OsStr, fmt::Display, path::PathBuf, process::exit, str::FromStr};
use anyhow::Error;
@ -12,6 +13,13 @@ pub mod argp_version;
pub mod cmd;
pub mod obj;
pub mod util;
pub mod vfs;
// musl's allocator is very slow, so use mimalloc when targeting musl.
// Otherwise, use the system allocator to avoid extra code size.
#[cfg(target_env = "musl")]
#[global_allocator]
static ALLOC: mimalloc::MiMalloc = mimalloc::MiMalloc;
#[derive(Debug, Eq, PartialEq, Copy, Clone)]
enum LogLevel {
@ -89,13 +97,13 @@ enum SubCommand {
Elf(cmd::elf::Args),
Elf2Dol(cmd::elf2dol::Args),
Map(cmd::map::Args),
MetroidBuildInfo(cmd::metroidbuildinfo::Args),
Nlzss(cmd::nlzss::Args),
Rarc(cmd::rarc::Args),
Rel(cmd::rel::Args),
Rso(cmd::rso::Args),
Shasum(cmd::shasum::Args),
U8(cmd::u8_arc::Args),
Vfs(cmd::vfs::Args),
Yay0(cmd::yay0::Args),
Yaz0(cmd::yaz0::Args),
}
@ -164,13 +172,13 @@ fn main() {
SubCommand::Elf(c_args) => cmd::elf::run(c_args),
SubCommand::Elf2Dol(c_args) => cmd::elf2dol::run(c_args),
SubCommand::Map(c_args) => cmd::map::run(c_args),
SubCommand::MetroidBuildInfo(c_args) => cmd::metroidbuildinfo::run(c_args),
SubCommand::Nlzss(c_args) => cmd::nlzss::run(c_args),
SubCommand::Rarc(c_args) => cmd::rarc::run(c_args),
SubCommand::Rel(c_args) => cmd::rel::run(c_args),
SubCommand::Rso(c_args) => cmd::rso::run(c_args),
SubCommand::Shasum(c_args) => cmd::shasum::run(c_args),
SubCommand::U8(c_args) => cmd::u8_arc::run(c_args),
SubCommand::Vfs(c_args) => cmd::vfs::run(c_args),
SubCommand::Yay0(c_args) => cmd::yay0::run(c_args),
SubCommand::Yaz0(c_args) => cmd::yaz0::run(c_args),
});

View File

@ -13,7 +13,9 @@ use std::{
use anyhow::{anyhow, bail, ensure, Result};
use objdiff_core::obj::split_meta::SplitMeta;
pub use relocations::{ObjReloc, ObjRelocKind, ObjRelocations};
pub use sections::{ObjSection, ObjSectionKind, ObjSections};
pub use sections::{
section_kind_for_section, ObjSection, ObjSectionKind, ObjSections, SectionIndex,
};
pub use splits::{ObjSplit, ObjSplits};
pub use symbols::{
best_match_for_reloc, ObjDataKind, ObjSymbol, ObjSymbolFlagSet, ObjSymbolFlags, ObjSymbolKind,
@ -47,6 +49,8 @@ pub struct ObjUnit {
pub autogenerated: bool,
/// MW `.comment` section version.
pub comment_version: Option<u8>,
/// Influences the order of this unit relative to other ordered units.
pub order: Option<i32>,
}
#[derive(Debug, Clone)]
@ -130,7 +134,12 @@ impl ObjInfo {
self.symbols.add(in_symbol, replace)
}
pub fn add_split(&mut self, section_index: usize, address: u32, split: ObjSplit) -> Result<()> {
pub fn add_split(
&mut self,
section_index: SectionIndex,
address: u32,
split: ObjSplit,
) -> Result<()> {
let section = self
.sections
.get_mut(section_index)
@ -320,8 +329,8 @@ impl ObjInfo {
// Include common symbols
self.symbols
.iter()
.filter(|&symbol| symbol.flags.is_common())
.map(|s| s.size as u32),
.filter(|&(_, symbol)| symbol.flags.is_common())
.map(|(_, s)| s.size as u32),
)
.sum()
}

View File

@ -7,7 +7,10 @@ use std::{
use anyhow::{anyhow, bail, ensure, Result};
use itertools::Itertools;
use crate::obj::{ObjKind, ObjRelocations, ObjSplit, ObjSplits, ObjSymbol};
use crate::{
analysis::cfa::SectionAddress,
obj::{ObjKind, ObjRelocations, ObjSplit, ObjSplits, ObjSymbol},
};
#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)]
pub enum ObjSectionKind {
@ -26,7 +29,7 @@ pub struct ObjSection {
pub data: Vec<u8>,
pub align: u64,
/// REL files reference the original ELF section indices
pub elf_index: usize,
pub elf_index: SectionIndex,
pub relocations: ObjRelocations,
pub virtual_address: Option<u64>,
pub file_offset: u64,
@ -40,38 +43,45 @@ pub struct ObjSections {
sections: Vec<ObjSection>,
}
pub type SectionIndex = u32;
impl ObjSections {
pub fn new(obj_kind: ObjKind, sections: Vec<ObjSection>) -> Self { Self { obj_kind, sections } }
pub fn iter(&self) -> impl DoubleEndedIterator<Item = (usize, &ObjSection)> {
self.sections.iter().enumerate()
pub fn iter(&self) -> impl DoubleEndedIterator<Item = (SectionIndex, &ObjSection)> {
self.sections.iter().enumerate().map(|(i, s)| (i as SectionIndex, s))
}
pub fn iter_mut(&mut self) -> impl DoubleEndedIterator<Item = (usize, &mut ObjSection)> {
self.sections.iter_mut().enumerate()
pub fn iter_mut(&mut self) -> impl DoubleEndedIterator<Item = (SectionIndex, &mut ObjSection)> {
self.sections.iter_mut().enumerate().map(|(i, s)| (i as SectionIndex, s))
}
pub fn len(&self) -> usize { self.sections.len() }
pub fn len(&self) -> SectionIndex { self.sections.len() as SectionIndex }
pub fn is_empty(&self) -> bool { self.sections.is_empty() }
pub fn next_section_index(&self) -> usize { self.sections.len() }
pub fn next_section_index(&self) -> SectionIndex { self.sections.len() as SectionIndex }
pub fn get(&self, index: usize) -> Option<&ObjSection> { self.sections.get(index) }
pub fn get_mut(&mut self, index: usize) -> Option<&mut ObjSection> {
self.sections.get_mut(index)
pub fn get(&self, index: SectionIndex) -> Option<&ObjSection> {
self.sections.get(index as usize)
}
pub fn get_elf_index(&self, elf_index: usize) -> Option<(usize, &ObjSection)> {
pub fn get_mut(&mut self, index: SectionIndex) -> Option<&mut ObjSection> {
self.sections.get_mut(index as usize)
}
pub fn get_elf_index(&self, elf_index: SectionIndex) -> Option<(SectionIndex, &ObjSection)> {
self.iter().find(|&(_, s)| s.elf_index == elf_index)
}
pub fn get_elf_index_mut(&mut self, elf_index: usize) -> Option<(usize, &mut ObjSection)> {
pub fn get_elf_index_mut(
&mut self,
elf_index: SectionIndex,
) -> Option<(SectionIndex, &mut ObjSection)> {
self.iter_mut().find(|(_, s)| s.elf_index == elf_index)
}
pub fn at_address(&self, addr: u32) -> Result<(usize, &ObjSection)> {
pub fn at_address(&self, addr: u32) -> Result<(SectionIndex, &ObjSection)> {
ensure!(
self.obj_kind == ObjKind::Executable,
"Use of ObjSections::at_address in relocatable object"
@ -81,7 +91,7 @@ impl ObjSections {
.ok_or_else(|| anyhow!("Failed to locate section @ {:#010X}", addr))
}
pub fn at_address_mut(&mut self, addr: u32) -> Result<(usize, &mut ObjSection)> {
pub fn at_address_mut(&mut self, addr: u32) -> Result<(SectionIndex, &mut ObjSection)> {
ensure!(
self.obj_kind == ObjKind::Executable,
"Use of ObjSections::at_address_mut in relocatable object"
@ -91,7 +101,7 @@ impl ObjSections {
.ok_or_else(|| anyhow!("Failed to locate section @ {:#010X}", addr))
}
pub fn with_range(&self, range: Range<u32>) -> Result<(usize, &ObjSection)> {
pub fn with_range(&self, range: Range<u32>) -> Result<(SectionIndex, &ObjSection)> {
ensure!(
self.obj_kind == ObjKind::Executable,
"Use of ObjSections::with_range in relocatable object"
@ -104,46 +114,52 @@ impl ObjSections {
pub fn by_kind(
&self,
kind: ObjSectionKind,
) -> impl DoubleEndedIterator<Item = (usize, &ObjSection)> {
) -> impl DoubleEndedIterator<Item = (SectionIndex, &ObjSection)> {
self.iter().filter(move |(_, s)| s.kind == kind)
}
pub fn by_name(&self, name: &str) -> Result<Option<(usize, &ObjSection)>> {
pub fn by_name(&self, name: &str) -> Result<Option<(SectionIndex, &ObjSection)>> {
self.iter()
.filter(move |(_, s)| s.name == name)
.at_most_one()
.map_err(|_| anyhow!("Multiple sections with name {}", name))
}
pub fn push(&mut self, section: ObjSection) -> usize {
pub fn push(&mut self, section: ObjSection) -> SectionIndex {
let index = self.sections.len();
self.sections.push(section);
index
index as SectionIndex
}
pub fn all_splits(
&self,
) -> impl DoubleEndedIterator<Item = (usize, &ObjSection, u32, &ObjSplit)> {
) -> impl DoubleEndedIterator<Item = (SectionIndex, &ObjSection, u32, &ObjSplit)> {
self.iter()
.flat_map(|(idx, s)| s.splits.iter().map(move |(addr, split)| (idx, s, addr, split)))
}
pub fn common_bss_start(&self) -> Option<(usize, u32)> {
pub fn common_bss_start(&self) -> Option<SectionAddress> {
let Ok(Some((section_index, section))) = self.by_name(".bss") else {
return None;
};
section.splits.iter().find(|(_, split)| split.common).map(|(addr, _)| (section_index, addr))
section
.splits
.iter()
.find(|(_, split)| split.common)
.map(|(addr, _)| SectionAddress::new(section_index, addr))
}
}
impl Index<usize> for ObjSections {
impl Index<SectionIndex> for ObjSections {
type Output = ObjSection;
fn index(&self, index: usize) -> &Self::Output { &self.sections[index] }
fn index(&self, index: SectionIndex) -> &Self::Output { &self.sections[index as usize] }
}
impl IndexMut<usize> for ObjSections {
fn index_mut(&mut self, index: usize) -> &mut Self::Output { &mut self.sections[index] }
impl IndexMut<SectionIndex> for ObjSections {
fn index_mut(&mut self, index: SectionIndex) -> &mut Self::Output {
&mut self.sections[index as usize]
}
}
impl ObjSection {
@ -218,7 +234,7 @@ impl ObjSection {
}
}
fn section_kind_for_section(section_name: &str) -> Result<ObjSectionKind> {
pub fn section_kind_for_section(section_name: &str) -> Result<ObjSectionKind> {
Ok(match section_name {
".init" | ".text" | ".dbgtext" | ".vmtext" => ObjSectionKind::Code,
".ctors" | ".dtors" | ".rodata" | ".sdata2" | "extab" | "extabindex" | ".BINARY" => {

View File

@ -4,7 +4,7 @@ use anyhow::{anyhow, Result};
use itertools::Itertools;
use crate::{
obj::{ObjInfo, ObjSection},
obj::{ObjInfo, ObjSection, SectionIndex},
util::{nested::NestedVec, split::default_section_align},
};
@ -28,7 +28,7 @@ impl ObjSplit {
pub fn alignment(
&self,
obj: &ObjInfo,
section_index: usize,
section_index: SectionIndex,
section: &ObjSection,
split_addr: u32,
) -> u32 {

View File

@ -12,7 +12,7 @@ use serde_repr::{Deserialize_repr, Serialize_repr};
use crate::{
analysis::cfa::SectionAddress,
obj::{ObjKind, ObjRelocKind, ObjSections},
obj::{sections::SectionIndex, ObjKind, ObjRelocKind, ObjSections},
util::{
config::{is_auto_jump_table, is_auto_label, is_auto_symbol, parse_u32},
nested::NestedVec,
@ -187,7 +187,7 @@ pub struct ObjSymbol {
pub name: String,
pub demangled_name: Option<String>,
pub address: u64,
pub section: Option<usize>,
pub section: Option<SectionIndex>,
pub size: u64,
pub size_known: bool,
pub flags: ObjSymbolFlagSet,
@ -199,7 +199,7 @@ pub struct ObjSymbol {
pub demangled_name_hash: Option<u32>,
}
pub type SymbolIndex = usize;
pub type SymbolIndex = u32;
#[derive(Debug, Clone)]
pub struct ObjSymbols {
@ -216,8 +216,10 @@ impl ObjSymbols {
let mut symbols_by_section: Vec<BTreeMap<u32, Vec<SymbolIndex>>> = vec![];
let mut symbols_by_name = HashMap::<String, Vec<SymbolIndex>>::new();
for (idx, symbol) in symbols.iter().enumerate() {
let idx = idx as SymbolIndex;
symbols_by_address.nested_push(symbol.address as u32, idx);
if let Some(section_idx) = symbol.section {
let section_idx = section_idx as usize;
if section_idx >= symbols_by_section.len() {
symbols_by_section.resize_with(section_idx + 1, BTreeMap::new);
}
@ -312,7 +314,7 @@ impl ObjSymbols {
}
symbol_idx
} else {
let target_symbol_idx = self.symbols.len();
let target_symbol_idx = self.symbols.len() as SymbolIndex;
self.add_direct(ObjSymbol {
name: in_symbol.name,
demangled_name: in_symbol.demangled_name,
@ -333,9 +335,10 @@ impl ObjSymbols {
}
pub fn add_direct(&mut self, in_symbol: ObjSymbol) -> Result<SymbolIndex> {
let symbol_idx = self.symbols.len();
let symbol_idx = self.symbols.len() as SymbolIndex;
self.symbols_by_address.nested_push(in_symbol.address as u32, symbol_idx);
if let Some(section_idx) = in_symbol.section {
let section_idx = section_idx as usize;
if section_idx >= self.symbols_by_section.len() {
self.symbols_by_section.resize_with(section_idx + 1, BTreeMap::new);
}
@ -355,28 +358,30 @@ impl ObjSymbols {
Ok(symbol_idx)
}
pub fn iter(&self) -> impl DoubleEndedIterator<Item = &ObjSymbol> { self.symbols.iter() }
pub fn iter(&self) -> impl DoubleEndedIterator<Item = (SymbolIndex, &ObjSymbol)> {
self.symbols.iter().enumerate().map(|(i, s)| (i as SymbolIndex, s))
}
pub fn count(&self) -> usize { self.symbols.len() }
pub fn count(&self) -> SymbolIndex { self.symbols.len() as SymbolIndex }
pub fn at_section_address(
&self,
section_idx: usize,
section_idx: SectionIndex,
addr: u32,
) -> impl DoubleEndedIterator<Item = (SymbolIndex, &ObjSymbol)> {
self.symbols_by_section
.get(section_idx)
.get(section_idx as usize)
.and_then(|v| v.get(&addr))
.into_iter()
.flatten()
.map(move |&idx| (idx, &self.symbols[idx]))
.map(move |&idx| (idx, &self.symbols[idx as usize]))
// "Stripped" symbols don't actually exist at the address
.filter(|(_, sym)| !sym.flags.is_stripped())
}
pub fn kind_at_section_address(
&self,
section_idx: usize,
section_idx: SectionIndex,
addr: u32,
kind: ObjSymbolKind,
) -> Result<Option<(SymbolIndex, &ObjSymbol)>> {
@ -397,7 +402,7 @@ impl ObjSymbols {
self.symbols_by_section
.iter()
.flat_map(|v| v.iter().map(|(_, v)| v))
.flat_map(move |v| v.iter().map(move |u| (*u, &self.symbols[*u])))
.flat_map(move |v| v.iter().map(move |u| (*u, &self.symbols[*u as usize])))
}
// Iterate over all ABS symbols
@ -405,24 +410,24 @@ impl ObjSymbols {
debug_assert!(self.obj_kind == ObjKind::Executable);
self.symbols_by_address
.iter()
.flat_map(|(_, v)| v.iter().map(|&u| (u, &self.symbols[u])))
.flat_map(|(_, v)| v.iter().map(|&u| (u, &self.symbols[u as usize])))
.filter(|(_, s)| s.section.is_none())
}
// Iterate over range in address ascending order, excluding ABS symbols
pub fn for_section_range<R>(
&self,
section_index: usize,
section_index: SectionIndex,
range: R,
) -> impl DoubleEndedIterator<Item = (SymbolIndex, &ObjSymbol)>
where
R: RangeBounds<u32> + Clone,
{
self.symbols_by_section
.get(section_index)
.get(section_index as usize)
.into_iter()
.flat_map(move |v| v.range(range.clone()))
.flat_map(move |(_, v)| v.iter().map(move |u| (*u, &self.symbols[*u])))
.flat_map(move |(_, v)| v.iter().map(move |u| (*u, &self.symbols[*u as usize])))
}
pub fn indexes_for_range<R>(
@ -438,13 +443,13 @@ impl ObjSymbols {
pub fn for_section(
&self,
section_idx: usize,
section_idx: SectionIndex,
) -> impl DoubleEndedIterator<Item = (SymbolIndex, &ObjSymbol)> {
self.symbols_by_section
.get(section_idx)
.get(section_idx as usize)
.into_iter()
.flat_map(|v| v.iter().map(|(_, v)| v))
.flat_map(move |v| v.iter().map(move |u| (*u, &self.symbols[*u])))
.flat_map(move |v| v.iter().map(move |u| (*u, &self.symbols[*u as usize])))
}
pub fn for_name(
@ -454,7 +459,7 @@ impl ObjSymbols {
self.symbols_by_name
.get(name)
.into_iter()
.flat_map(move |v| v.iter().map(move |u| (*u, &self.symbols[*u])))
.flat_map(move |v| v.iter().map(move |u| (*u, &self.symbols[*u as usize])))
}
pub fn by_name(&self, name: &str) -> Result<Option<(SymbolIndex, &ObjSymbol)>> {
@ -515,11 +520,11 @@ impl ObjSymbols {
&self,
kind: ObjSymbolKind,
) -> impl DoubleEndedIterator<Item = (SymbolIndex, &ObjSymbol)> {
self.symbols.iter().enumerate().filter(move |(_, sym)| sym.kind == kind)
self.iter().filter(move |(_, sym)| sym.kind == kind)
}
pub fn replace(&mut self, index: SymbolIndex, symbol: ObjSymbol) -> Result<()> {
let symbol_ref = &mut self.symbols[index];
let symbol_ref = &mut self.symbols[index as usize];
ensure!(symbol_ref.address == symbol.address, "Can't modify address with replace_symbol");
ensure!(symbol_ref.section == symbol.section, "Can't modify section with replace_symbol");
if symbol_ref.name != symbol.name {
@ -545,7 +550,7 @@ impl ObjSymbols {
for (_addr, symbol_idxs) in self.indexes_for_range(..=target_addr.address).rev() {
let symbols = symbol_idxs
.iter()
.map(|&idx| (idx, &self.symbols[idx]))
.map(|&idx| (idx, &self.symbols[idx as usize]))
.filter(|(_, sym)| {
(sym.section.is_none() || sym.section == Some(target_addr.section))
&& sym.referenced_by(reloc_kind)
@ -570,14 +575,14 @@ impl ObjSymbols {
#[inline]
pub fn flags(&mut self, idx: SymbolIndex) -> &mut ObjSymbolFlagSet {
&mut self.symbols[idx].flags
&mut self.symbols[idx as usize].flags
}
}
impl Index<SymbolIndex> for ObjSymbols {
type Output = ObjSymbol;
fn index(&self, index: usize) -> &Self::Output { &self.symbols[index] }
fn index(&self, index: SymbolIndex) -> &Self::Output { &self.symbols[index as usize] }
}
impl ObjSymbol {

View File

@ -7,7 +7,7 @@ use anyhow::Result;
use io::{Error, ErrorKind};
use crate::{
obj::{ObjSymbol, ObjSymbolKind},
obj::{ObjSymbol, ObjSymbolKind, SectionIndex},
util::{
dol::{DolLike, DolSection, DolSectionKind},
reader::{
@ -55,7 +55,7 @@ impl FromReader for AlfFile {
data_size: section.data_size,
size: section.size,
kind,
index: sections.len(),
index: sections.len() as SectionIndex,
});
}
for sym in &symtab.symbols {
@ -230,7 +230,7 @@ impl AlfSymbol {
name,
demangled_name,
address: self.address as u64,
section: Some(self.section as usize - 1),
section: Some(self.section as SectionIndex - 1),
size: self.size as u64,
size_known: true,
flags: Default::default(),

View File

@ -11,7 +11,7 @@ use ppc750cl::{Argument, Ins, InsIter, Opcode};
use crate::{
obj::{
ObjDataKind, ObjInfo, ObjReloc, ObjRelocKind, ObjSection, ObjSectionKind, ObjSymbol,
ObjSymbolKind,
ObjSymbolKind, SymbolIndex,
},
util::nested::NestedVec,
};
@ -25,7 +25,7 @@ enum SymbolEntryKind {
#[derive(Debug, Copy, Clone, Eq, PartialEq)]
struct SymbolEntry {
index: usize,
index: SymbolIndex,
kind: SymbolEntryKind,
}
@ -44,7 +44,7 @@ where W: Write + ?Sized {
}
// We'll append generated symbols to the end
let mut symbols: Vec<ObjSymbol> = obj.symbols.iter().cloned().collect();
let mut symbols: Vec<ObjSymbol> = obj.symbols.iter().map(|(_, s)| s.clone()).collect();
let mut section_entries: Vec<BTreeMap<u32, Vec<SymbolEntry>>> = vec![];
let mut section_relocations: Vec<BTreeMap<u32, ObjReloc>> = vec![];
for (section_idx, section) in obj.sections.iter() {
@ -90,7 +90,7 @@ where W: Write + ?Sized {
.map(|e| e.index);
if target_symbol_idx.is_none() {
let display_address = address as u64 + section.virtual_address.unwrap_or(0);
let symbol_idx = symbols.len();
let symbol_idx = symbols.len() as SymbolIndex;
symbols.push(ObjSymbol {
name: format!(".L_{display_address:08X}"),
address: display_address,
@ -131,7 +131,7 @@ where W: Write + ?Sized {
if reloc.addend == 0 {
continue;
}
let target = &symbols[reloc.target_symbol];
let target = &symbols[reloc.target_symbol as usize];
let target_section_idx = match target.section {
Some(v) => v,
None => continue,
@ -140,7 +140,7 @@ where W: Write + ?Sized {
anyhow!("Invalid relocation target section: {:#010X} {:?}", reloc_address, target)
})?;
let address = (target.address as i64 + reloc.addend) as u64;
let vec = match section_entries[target_section_idx].entry(address as u32) {
let vec = match section_entries[target_section_idx as usize].entry(address as u32) {
btree_map::Entry::Occupied(e) => e.into_mut(),
btree_map::Entry::Vacant(e) => e.insert(vec![]),
};
@ -149,7 +149,7 @@ where W: Write + ?Sized {
.any(|e| e.kind == SymbolEntryKind::Label || e.kind == SymbolEntryKind::Start)
{
let display_address = address + target_section.virtual_address.unwrap_or(0);
let symbol_idx = symbols.len();
let symbol_idx = symbols.len() as SymbolIndex;
symbols.push(ObjSymbol {
name: format!(".L_{display_address:08X}"),
address: display_address,
@ -181,13 +181,17 @@ where W: Write + ?Sized {
}
for (section_index, section) in obj.sections.iter() {
let entries = &section_entries[section_index];
let relocations = &section_relocations[section_index];
let entries = &section_entries[section_index as usize];
let relocations = &section_relocations[section_index as usize];
let mut current_address = section.address as u32;
let section_end = (section.address + section.size) as u32;
let subsection =
obj.sections.iter().take(section_index).filter(|(_, s)| s.name == section.name).count();
let subsection = obj
.sections
.iter()
.take(section_index as usize)
.filter(|(_, s)| s.name == section.name)
.count();
loop {
if current_address >= section_end {
@ -369,7 +373,7 @@ fn write_symbol_entry<W>(
where
W: Write + ?Sized,
{
let symbol = &symbols[entry.index];
let symbol = &symbols[entry.index as usize];
// Skip writing certain symbols
if symbol.kind == ObjSymbolKind::Section {
@ -432,9 +436,37 @@ where
write_symbol_name(w, &symbol.name)?;
writeln!(w)?;
}
if entry.kind == SymbolEntryKind::Start && section.name == "extab" {
writeln!(w, "/*")?;
match parse_extab(symbols, entry, section) {
Ok(s) => {
for line in s.trim_end().lines() {
writeln!(w, " * {}", line)?;
}
}
Err(e) => {
log::warn!("Failed to decode extab entry {}: {}", symbol.name, e);
writeln!(w, " * Failed to decode extab entry: {}", e)?;
}
}
writeln!(w, " */")?;
}
Ok(())
}
fn parse_extab(symbols: &[ObjSymbol], entry: &SymbolEntry, section: &ObjSection) -> Result<String> {
let symbol = &symbols[entry.index as usize];
let data = section.symbol_data(symbol)?;
let decoded = cwextab::decode_extab(data)?;
let function_names = section
.relocations
.range(symbol.address as u32..(symbol.address + symbol.size) as u32)
.map(|(_, reloc)| symbols[reloc.target_symbol as usize].name.clone())
.collect_vec();
decoded.to_string(function_names).ok_or_else(|| anyhow!("Failed to print extab entry"))
}
#[allow(clippy::too_many_arguments)]
fn write_data<W>(
w: &mut W,
@ -476,7 +508,7 @@ where
.with_context(|| format!("At address {:#010X}", sym_addr))?;
entry = entry_iter.next();
} else if current_address > sym_addr {
let dbg_symbols = vec.iter().map(|e| &symbols[e.index]).collect_vec();
let dbg_symbols = vec.iter().map(|e| &symbols[e.index as usize]).collect_vec();
bail!(
"Unaligned symbol entry @ {:#010X}:\n\t{:?}",
section.virtual_address.unwrap_or(0) as u32 + sym_addr,
@ -557,7 +589,7 @@ fn find_symbol_kind(
for entry in entries {
match entry.kind {
SymbolEntryKind::Start => {
let new_kind = symbols[entry.index].kind;
let new_kind = symbols[entry.index as usize].kind;
if !matches!(new_kind, ObjSymbolKind::Unknown | ObjSymbolKind::Section) {
ensure!(
!found || new_kind == kind,
@ -583,11 +615,11 @@ fn find_data_kind(
for entry in entries {
match entry.kind {
SymbolEntryKind::Start => {
let new_kind = symbols[entry.index].data_kind;
let new_kind = symbols[entry.index as usize].data_kind;
if !matches!(new_kind, ObjDataKind::Unknown) {
if found && new_kind != kind {
for entry in entries {
log::error!("Symbol {:?}", symbols[entry.index]);
log::error!("Symbol {:?}", symbols[entry.index as usize]);
}
bail!(
"Conflicting data kinds found: {kind:?} and {new_kind:?}",
@ -778,14 +810,14 @@ where
ObjRelocKind::Absolute => {
// Attempt to use .rel macro for relative relocations
if reloc.addend != 0 {
let target = &symbols[reloc.target_symbol];
let target = &symbols[reloc.target_symbol as usize];
let target_addr = (target.address as i64 + reloc.addend) as u32;
if let Some(entry) = target
.section
.and_then(|section_idx| section_entries[section_idx].get(&target_addr))
.and_then(|section_idx| section_entries[section_idx as usize].get(&target_addr))
.and_then(|entries| entries.iter().find(|e| e.kind == SymbolEntryKind::Label))
{
let symbol = &symbols[entry.index];
let symbol = &symbols[entry.index as usize];
write!(w, "\t.rel ")?;
write_symbol_name(w, &target.name)?;
write!(w, ", ")?;
@ -931,7 +963,7 @@ fn write_reloc_symbol<W>(
where
W: Write + ?Sized,
{
write_symbol_name(w, &symbols[reloc.target_symbol].name)?;
write_symbol_name(w, &symbols[reloc.target_symbol as usize].name)?;
match reloc.addend.cmp(&0i64) {
Ordering::Greater => write!(w, "+{:#X}", reloc.addend),
Ordering::Less => write!(w, "-{:#X}", -reloc.addend),

View File

@ -2,7 +2,6 @@ use std::{
fs,
io::{BufRead, Write},
num::ParseIntError,
path::Path,
str::FromStr,
};
@ -12,18 +11,20 @@ use filetime::FileTime;
use once_cell::sync::Lazy;
use regex::{Captures, Regex};
use tracing::{debug, info, warn};
use typed_path::Utf8NativePath;
use xxhash_rust::xxh3::xxh3_64;
use crate::{
analysis::cfa::SectionAddress,
obj::{
ObjDataKind, ObjInfo, ObjKind, ObjSectionKind, ObjSplit, ObjSymbol, ObjSymbolFlagSet,
ObjSymbolFlags, ObjSymbolKind, ObjUnit,
ObjSymbolFlags, ObjSymbolKind, ObjUnit, SectionIndex,
},
util::{
file::{buf_writer, map_file, FileReadInfo},
file::{buf_writer, FileReadInfo},
split::default_section_align,
},
vfs::open_file,
};
pub fn parse_u32(s: &str) -> Result<u32, ParseIntError> {
@ -34,12 +35,24 @@ pub fn parse_u32(s: &str) -> Result<u32, ParseIntError> {
}
}
pub fn apply_symbols_file<P>(path: P, obj: &mut ObjInfo) -> Result<Option<FileReadInfo>>
where P: AsRef<Path> {
Ok(if path.as_ref().is_file() {
let file = map_file(path)?;
let cached = FileReadInfo::new(&file)?;
for result in file.as_reader().lines() {
pub fn parse_i32(s: &str) -> Result<i32, ParseIntError> {
if let Some(s) = s.strip_prefix("-0x").or_else(|| s.strip_prefix("-0X")) {
i32::from_str_radix(s, 16).map(|v| -v)
} else if let Some(s) = s.strip_prefix("0x").or_else(|| s.strip_prefix("0X")) {
i32::from_str_radix(s, 16)
} else {
s.parse::<i32>()
}
}
pub fn apply_symbols_file(
path: &Utf8NativePath,
obj: &mut ObjInfo,
) -> Result<Option<FileReadInfo>> {
Ok(if fs::metadata(path).is_ok_and(|m| m.is_file()) {
let mut file = open_file(path, true)?;
let cached = FileReadInfo::new(file.as_mut())?;
for result in file.lines() {
let line = match result {
Ok(line) => line,
Err(e) => bail!("Failed to process symbols file: {e:?}"),
@ -187,19 +200,21 @@ pub fn is_auto_label(symbol: &ObjSymbol) -> bool { symbol.name.starts_with("lbl_
pub fn is_auto_jump_table(symbol: &ObjSymbol) -> bool { symbol.name.starts_with("jumptable_") }
fn write_if_unchanged<P, Cb>(path: P, cb: Cb, cached_file: Option<FileReadInfo>) -> Result<()>
fn write_if_unchanged<Cb>(
path: &Utf8NativePath,
cb: Cb,
cached_file: Option<FileReadInfo>,
) -> Result<()>
where
P: AsRef<Path>,
Cb: FnOnce(&mut dyn Write) -> Result<()>,
{
if let Some(cached_file) = cached_file {
// Check file mtime
let path = path.as_ref();
let new_mtime = fs::metadata(path).ok().map(|m| FileTime::from_last_modification_time(&m));
if let Some(new_mtime) = new_mtime {
if new_mtime != cached_file.mtime {
if let (Some(new_mtime), Some(old_mtime)) = (new_mtime, cached_file.mtime) {
if new_mtime != old_mtime {
// File changed, don't write
warn!(path = %path.display(), "File changed since read, not updating");
warn!(path = %path, "File changed since read, not updating");
return Ok(());
}
}
@ -209,12 +224,12 @@ where
cb(&mut buf)?;
if xxh3_64(&buf) == cached_file.hash {
// No changes
debug!(path = %path.display(), "File unchanged");
debug!(path = %path, "File unchanged");
return Ok(());
}
// Write to file
info!("Writing updated {}", path.display());
info!("Writing updated {}", path);
fs::write(path, &buf)?;
} else {
// Write directly
@ -226,14 +241,11 @@ where
}
#[inline]
pub fn write_symbols_file<P>(
path: P,
pub fn write_symbols_file(
path: &Utf8NativePath,
obj: &ObjInfo,
cached_file: Option<FileReadInfo>,
) -> Result<()>
where
P: AsRef<Path>,
{
) -> Result<()> {
write_if_unchanged(path, |w| write_symbols(w, obj), cached_file)
}
@ -401,15 +413,12 @@ fn section_kind_to_str(kind: ObjSectionKind) -> &'static str {
}
#[inline]
pub fn write_splits_file<P>(
path: P,
pub fn write_splits_file(
path: &Utf8NativePath,
obj: &ObjInfo,
all: bool,
cached_file: Option<FileReadInfo>,
) -> Result<()>
where
P: AsRef<Path>,
{
) -> Result<()> {
write_if_unchanged(path, |w| write_splits(w, obj, all), cached_file)
}
@ -428,6 +437,9 @@ where W: Write + ?Sized {
if let Some(comment_version) = unit.comment_version {
write!(w, " comment:{}", comment_version)?;
}
if let Some(order) = unit.order {
write!(w, " order:{}", order)?;
}
writeln!(w)?;
let mut split_iter = obj.sections.all_splits().peekable();
while let Some((_section_index, section, addr, split)) = split_iter.next() {
@ -475,6 +487,8 @@ struct SplitUnit {
name: String,
/// MW `.comment` section version
comment_version: Option<u8>,
/// Influences the order of this unit relative to other ordered units.
order: Option<i32>,
}
pub struct SectionDef {
@ -515,12 +529,13 @@ fn parse_unit_line(captures: Captures) -> Result<SplitLine> {
if name == "Sections" {
return Ok(SplitLine::SectionsStart);
}
let mut unit = SplitUnit { name: name.to_string(), comment_version: None };
let mut unit = SplitUnit { name: name.to_string(), comment_version: None, order: None };
for attr in captures["attrs"].split(' ').filter(|&s| !s.is_empty()) {
if let Some((attr, value)) = attr.split_once(':') {
match attr {
"comment" => unit.comment_version = Some(u8::from_str(value)?),
"order" => unit.order = Some(parse_i32(value)?),
_ => bail!("Unknown unit attribute '{}'", attr),
}
} else {
@ -603,16 +618,15 @@ fn parse_section_line(captures: Captures, state: &SplitState) -> Result<SplitLin
enum SplitState {
None,
Sections(usize),
Sections(SectionIndex),
Unit(String),
}
pub fn apply_splits_file<P>(path: P, obj: &mut ObjInfo) -> Result<Option<FileReadInfo>>
where P: AsRef<Path> {
Ok(if path.as_ref().is_file() {
let file = map_file(path)?;
let cached = FileReadInfo::new(&file)?;
apply_splits(&mut file.as_reader(), obj)?;
pub fn apply_splits_file(path: &Utf8NativePath, obj: &mut ObjInfo) -> Result<Option<FileReadInfo>> {
Ok(if fs::metadata(path).is_ok_and(|m| m.is_file()) {
let mut file = open_file(path, true)?;
let cached = FileReadInfo::new(file.as_mut())?;
apply_splits(file.as_mut(), obj)?;
Some(cached)
} else {
None
@ -631,12 +645,13 @@ where R: BufRead + ?Sized {
match (&mut state, split_line) {
(
SplitState::None | SplitState::Unit(_) | SplitState::Sections(_),
SplitLine::Unit(SplitUnit { name, comment_version }),
SplitLine::Unit(SplitUnit { name, comment_version, order }),
) => {
obj.link_order.push(ObjUnit {
name: name.clone(),
autogenerated: false,
comment_version,
order,
});
state = SplitState::Unit(name);
}
@ -718,16 +733,14 @@ where R: BufRead + ?Sized {
Ok(())
}
pub fn read_splits_sections<P>(path: P) -> Result<Option<Vec<SectionDef>>>
where P: AsRef<Path> {
if !path.as_ref().is_file() {
pub fn read_splits_sections(path: &Utf8NativePath) -> Result<Option<Vec<SectionDef>>> {
if !fs::metadata(path).is_ok_and(|m| m.is_file()) {
return Ok(None);
}
let file = map_file(path)?;
let r = file.as_reader();
let file = open_file(path, true)?;
let mut sections = Vec::new();
let mut state = SplitState::None;
for result in r.lines() {
for result in file.lines() {
let line = match result {
Ok(line) => line,
Err(e) => return Err(e.into()),

View File

@ -1,40 +1,39 @@
use std::{
io::Write,
path::{Path, PathBuf},
};
use std::io::Write;
use itertools::Itertools;
use path_slash::PathBufExt;
use crate::util::file::split_path;
use typed_path::{Utf8NativePath, Utf8NativePathBuf, Utf8UnixPathBuf};
pub struct DepFile {
pub name: PathBuf,
pub dependencies: Vec<PathBuf>,
pub name: Utf8UnixPathBuf,
pub dependencies: Vec<Utf8UnixPathBuf>,
}
fn normalize_path(path: Utf8NativePathBuf) -> Utf8UnixPathBuf {
if let Some((a, _)) = path.as_str().split_once(':') {
Utf8NativePath::new(a).with_unix_encoding()
} else {
path.with_unix_encoding()
}
}
impl DepFile {
pub fn new(name: PathBuf) -> Self { Self { name, dependencies: vec![] } }
pub fn push<P>(&mut self, dependency: P)
where P: AsRef<Path> {
let path = split_path(dependency.as_ref())
.map(|(p, _)| p)
.unwrap_or_else(|_| dependency.as_ref().to_path_buf());
self.dependencies.push(path);
pub fn new(name: Utf8NativePathBuf) -> Self {
Self { name: name.with_unix_encoding(), dependencies: vec![] }
}
pub fn extend(&mut self, dependencies: Vec<PathBuf>) {
self.dependencies.extend(dependencies.iter().map(|dependency| {
split_path(dependency).map(|(p, _)| p).unwrap_or_else(|_| dependency.clone())
}));
pub fn push(&mut self, dependency: Utf8NativePathBuf) {
self.dependencies.push(normalize_path(dependency));
}
pub fn extend(&mut self, dependencies: Vec<Utf8NativePathBuf>) {
self.dependencies.extend(dependencies.into_iter().map(normalize_path));
}
pub fn write<W>(&self, w: &mut W) -> std::io::Result<()>
where W: Write + ?Sized {
write!(w, "{}:", self.name.to_slash_lossy())?;
write!(w, "{}:", self.name)?;
for dep in self.dependencies.iter().unique() {
write!(w, " \\\n {}", dep.to_slash_lossy().replace(' ', "\\ "))?;
write!(w, " \\\n {}", dep.as_str().replace(' ', "\\ "))?;
}
Ok(())
}

View File

@ -1,4 +1,5 @@
//! This includes helpers to convert between decomp-toolkit types and objdiff-core types.
//!
//! Eventually it'd be nice to share [ObjInfo] and related types between decomp-toolkit and
//! objdiff-core to avoid this conversion.
use std::{
@ -28,7 +29,7 @@ pub fn process_code(
section: &ObjSection,
config: &DiffObjConfig,
) -> Result<ProcessCodeResult> {
let arch = objdiff_core::arch::ppc::ObjArchPpc {};
let arch = objdiff_core::arch::ppc::ObjArchPpc { extab: None };
let orig_relocs = section
.relocations
.range(symbol.address as u32..symbol.address as u32 + symbol.size as u32)
@ -39,7 +40,7 @@ pub fn process_code(
arch.process_code(
symbol.address,
orig_data,
section.elf_index,
section.elf_index as usize,
&orig_relocs,
&Default::default(),
config,
@ -224,8 +225,11 @@ fn print_line(ins_diff: &ObjInsDiff, base_addr: u64) -> Vec<Span> {
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);
@ -287,6 +291,7 @@ fn to_objdiff_symbol(
if symbol.flags.is_hidden() {
flags.0 |= objdiff_core::obj::ObjSymbolFlags::Hidden;
}
let bytes = section.and_then(|s| s.symbol_data(symbol).ok()).map_or(vec![], |d| d.to_vec());
objdiff_core::obj::ObjSymbol {
name: symbol.name.clone(),
demangled_name: symbol.demangled_name.clone(),
@ -297,6 +302,8 @@ fn to_objdiff_symbol(
flags,
addend,
virtual_address: None,
original_index: None,
bytes,
}
}

View File

@ -11,7 +11,7 @@ use crate::{
array_ref,
obj::{
ObjArchitecture, ObjInfo, ObjKind, ObjSection, ObjSectionKind, ObjSymbol, ObjSymbolFlagSet,
ObjSymbolFlags, ObjSymbolKind,
ObjSymbolFlags, ObjSymbolKind, SectionIndex,
},
util::{
alf::{AlfFile, AlfSymbol, ALF_MAGIC},
@ -75,7 +75,7 @@ pub struct DolSection {
pub size: u32,
pub kind: DolSectionKind,
// TODO remove
pub index: usize,
pub index: SectionIndex,
}
#[derive(Debug, Clone)]
@ -103,7 +103,7 @@ impl FromReader for DolFile {
data_size: size,
size,
kind: DolSectionKind::Text,
index: sections.len(),
index: sections.len() as SectionIndex,
});
}
for (idx, &size) in header.data_sizes.iter().enumerate() {
@ -116,7 +116,7 @@ impl FromReader for DolFile {
data_size: size,
size,
kind: DolSectionKind::Data,
index: sections.len(),
index: sections.len() as SectionIndex,
});
}
sections.push(DolSection {
@ -125,7 +125,7 @@ impl FromReader for DolFile {
data_size: 0,
size: header.bss_size,
kind: DolSectionKind::Bss,
index: sections.len(),
index: sections.len() as SectionIndex,
});
Ok(Self { header, sections })
}
@ -286,8 +286,8 @@ pub fn process_dol(buf: &[u8], name: &str) -> Result<ObjInfo> {
dol.sections().iter().filter(|section| section.kind == DolSectionKind::Text).count();
let mut eti_entries: Vec<EtiEntry> = Vec::new();
let mut eti_init_info_range: Option<(u32, u32)> = None;
let mut extab_section: Option<usize> = None;
let mut extabindex_section: Option<usize> = None;
let mut extab_section: Option<SectionIndex> = None;
let mut extabindex_section: Option<SectionIndex> = None;
'outer: for dol_section in
dol.sections().iter().filter(|section| section.kind == DolSectionKind::Data)
{
@ -537,8 +537,9 @@ pub fn process_dol(buf: &[u8], name: &str) -> Result<ObjInfo> {
}
// Apply section indices
let mut init_section_index = None;
let mut init_section_index: Option<SectionIndex> = None;
for (idx, section) in sections.iter_mut().enumerate() {
let idx = idx as SectionIndex;
match section.name.as_str() {
".init" => {
init_section_index = Some(idx);

View File

@ -274,6 +274,7 @@ pub enum AttributeKind {
Prototyped = 0x0270 | (FormKind::String as u16),
Public = 0x0280 | (FormKind::String as u16),
PureVirtual = 0x0290 | (FormKind::String as u16),
PureVirtualBlock2 = 0x0290 | (FormKind::Block2 as u16),
ReturnAddr = 0x02a0 | (FormKind::Block2 as u16),
Specification = 0x02b0 | (FormKind::Ref as u16),
StartScope = 0x02c0 | (FormKind::Data4 as u16),
@ -283,6 +284,7 @@ pub enum AttributeKind {
UpperBoundData4 = 0x02f0 | (FormKind::Data4 as u16),
UpperBoundData8 = 0x02f0 | (FormKind::Data8 as u16),
Virtual = 0x0300 | (FormKind::String as u16),
VirtualBlock2 = 0x0300 | (FormKind::Block2 as u16),
LoUser = 0x2000,
HiUser = 0x3ff0,
// User types

View File

@ -12,7 +12,7 @@ use indexmap::IndexMap;
use objdiff_core::obj::split_meta::{SplitMeta, SHT_SPLITMETA, SPLITMETA_SECTION};
use object::{
elf,
elf::{SHF_ALLOC, SHF_EXECINSTR, SHF_WRITE, SHT_NOBITS, SHT_PROGBITS},
elf::{SHF_ALLOC, SHF_EXECINSTR, SHF_WRITE, SHT_LOUSER, SHT_NOBITS, SHT_PROGBITS},
write::{
elf::{ProgramHeader, Rel, SectionHeader, SectionIndex, SymbolIndex, Writer},
StringId,
@ -20,20 +20,24 @@ use object::{
Architecture, Endianness, Object, ObjectKind, ObjectSection, ObjectSymbol, Relocation,
RelocationFlags, RelocationTarget, SectionKind, Symbol, SymbolKind, SymbolScope, SymbolSection,
};
use typed_path::Utf8NativePath;
use crate::{
array_ref,
obj::{
ObjArchitecture, ObjInfo, ObjKind, ObjReloc, ObjRelocKind, ObjSection, ObjSectionKind,
ObjSplit, ObjSymbol, ObjSymbolFlagSet, ObjSymbolFlags, ObjSymbolKind, ObjUnit,
SectionIndex as ObjSectionIndex, SymbolIndex as ObjSymbolIndex,
},
util::{
comment::{CommentSym, MWComment},
file::map_file,
reader::{Endian, FromReader, ToWriter},
},
vfs::open_file,
};
pub const SHT_MWCATS: u32 = SHT_LOUSER + 0x4A2A82C2;
enum BoundaryState {
/// Looking for a file symbol, any section symbols are queued
LookForFile(Vec<(u64, String)>),
@ -43,10 +47,9 @@ enum BoundaryState {
FilesEnded,
}
pub fn process_elf<P>(path: P) -> Result<ObjInfo>
where P: AsRef<Path> {
let file = map_file(path)?;
let obj_file = object::read::File::parse(file.as_slice())?;
pub fn process_elf(path: &Utf8NativePath) -> Result<ObjInfo> {
let mut file = open_file(path, true)?;
let obj_file = object::read::File::parse(file.map()?)?;
let architecture = match obj_file.architecture() {
Architecture::PowerPc => ObjArchitecture::PowerPc,
arch => bail!("Unexpected architecture: {arch:?}"),
@ -68,7 +71,7 @@ where P: AsRef<Path> {
let mut sda2_base: Option<u32> = None;
let mut sections: Vec<ObjSection> = vec![];
let mut section_indexes: Vec<Option<usize>> = vec![];
let mut section_indexes: Vec<Option<usize>> = vec![None /* ELF null section */];
for section in obj_file.sections() {
if section.size() == 0 {
section_indexes.push(None);
@ -94,7 +97,7 @@ where P: AsRef<Path> {
size: section.size(),
data: section.uncompressed_data()?.to_vec(),
align: section.align(),
elf_index: section.index().0,
elf_index: section.index().0 as ObjSectionIndex,
relocations: Default::default(),
virtual_address: None, // Loaded from section symbol
file_offset: section.file_range().map(|(v, _)| v).unwrap_or_default(),
@ -113,6 +116,7 @@ where P: AsRef<Path> {
.context("While reading .comment section")?;
log::debug!("Loaded .comment section header {:?}", header);
let mut comment_syms = Vec::with_capacity(obj_file.symbols().count());
comment_syms.push(CommentSym::from_reader(&mut reader, Endian::Big)?); // ELF null symbol
for symbol in obj_file.symbols() {
let comment_sym = CommentSym::from_reader(&mut reader, Endian::Big)?;
log::debug!("Symbol {:?} -> Comment {:?}", symbol, comment_sym);
@ -147,7 +151,7 @@ where P: AsRef<Path> {
};
let mut symbols: Vec<ObjSymbol> = vec![];
let mut symbol_indexes: Vec<Option<usize>> = vec![];
let mut symbol_indexes: Vec<Option<ObjSymbolIndex>> = vec![None /* ELF null symbol */];
let mut section_starts = IndexMap::<String, Vec<(u64, String)>>::new();
let mut name_to_index = HashMap::<String, usize>::new(); // for resolving duplicate names
let mut boundary_state = BoundaryState::LookForFile(Default::default());
@ -235,7 +239,9 @@ where P: AsRef<Path> {
.and_then(|m| m.virtual_addresses.as_ref())
.and_then(|v| v.get(symbol.index().0).cloned())
{
sections[section_index.0].virtual_address = Some(addr);
if let Some(section_index) = section_indexes[section_index.0] {
sections[section_index].virtual_address = Some(addr);
}
}
let section = obj_file.section_by_index(section_index)?;
@ -297,13 +303,13 @@ where P: AsRef<Path> {
}
// Generate symbols
if matches!(symbol.kind(), SymbolKind::Null | SymbolKind::File)
if matches!(symbol.kind(), SymbolKind::File)
|| matches!(symbol.section_index(), Some(idx) if section_indexes[idx.0].is_none())
{
symbol_indexes.push(None);
continue;
}
symbol_indexes.push(Some(symbols.len()));
symbol_indexes.push(Some(symbols.len() as ObjSymbolIndex));
let align = mw_comment.as_ref().map(|(_, vec)| vec[symbol.index().0].align);
symbols.push(to_obj_symbol(&obj_file, &symbol, &section_indexes, align)?);
}
@ -316,6 +322,7 @@ where P: AsRef<Path> {
name: file_name.clone(),
autogenerated: false,
comment_version: None,
order: None,
});
}
@ -345,11 +352,12 @@ where P: AsRef<Path> {
// TODO rebuild common symbols
}
for (section_idx, section) in obj_file.sections().enumerate() {
let out_section = match section_indexes[section_idx].and_then(|idx| sections.get_mut(idx)) {
Some(s) => s,
None => continue,
};
for section in obj_file.sections() {
let out_section =
match section_indexes[section.index().0].and_then(|idx| sections.get_mut(idx)) {
Some(s) => s,
None => continue,
};
// Generate relocations
for (address, reloc) in section.relocations() {
let Some(reloc) =
@ -396,7 +404,7 @@ pub fn write_elf(obj: &ObjInfo, export_all: bool) -> Result<Vec<u8>> {
}
writer.reserve_null_section_index();
let mut out_sections: Vec<OutSection> = Vec::with_capacity(obj.sections.len());
let mut out_sections: Vec<OutSection> = Vec::with_capacity(obj.sections.len() as usize);
for (_, section) in obj.sections.iter() {
let name = writer.add_section_name(section.name.as_bytes());
let index = writer.reserve_section_index();
@ -411,7 +419,7 @@ pub fn write_elf(obj: &ObjInfo, export_all: bool) -> Result<Vec<u8>> {
});
}
let mut rela_names: Vec<String> = vec![Default::default(); obj.sections.len()];
let mut rela_names: Vec<String> = vec![Default::default(); obj.sections.len() as usize];
for (((_, section), out_section), rela_name) in
obj.sections.iter().zip(&mut out_sections).zip(&mut rela_names)
{
@ -444,7 +452,7 @@ pub fn write_elf(obj: &ObjInfo, export_all: bool) -> Result<Vec<u8>> {
});
// Generate .comment data
let mut comment_data = Vec::<u8>::with_capacity(0x2C + obj.symbols.count() * 8);
let mut comment_data = Vec::<u8>::with_capacity(0x2C + obj.symbols.count() as usize * 8);
mw_comment.to_writer_static(&mut comment_data, Endian::Big)?;
// Null symbol
CommentSym { align: 0, vis_flags: 0, active_flags: 0 }
@ -455,7 +463,7 @@ pub fn write_elf(obj: &ObjInfo, export_all: bool) -> Result<Vec<u8>> {
};
// Generate .note.split section
let mut split_meta = if let Some(metadata) = &obj.split_meta {
let mut split_meta = if let (Some(metadata), Some(_)) = (&obj.split_meta, &obj.mw_comment) {
// Reserve section
let name = writer.add_section_name(SPLITMETA_SECTION.as_bytes());
let index = writer.reserve_section_index();
@ -480,8 +488,8 @@ pub fn write_elf(obj: &ObjInfo, export_all: bool) -> Result<Vec<u8>> {
None
};
let mut out_symbols: Vec<OutSymbol> = Vec::with_capacity(obj.symbols.count());
let mut symbol_map = vec![None; obj.symbols.count()];
let mut out_symbols: Vec<OutSymbol> = Vec::with_capacity(obj.symbols.count() as usize);
let mut symbol_map = vec![None; obj.symbols.count() as usize];
let mut section_symbol_offset = 0;
let mut num_local = 0;
@ -527,7 +535,7 @@ pub fn write_elf(obj: &ObjInfo, export_all: bool) -> Result<Vec<u8>> {
// Add section symbols for relocatable objects
if obj.kind == ObjKind::Relocatable {
for (section_index, section) in obj.sections.iter() {
let out_section_index = out_sections.get(section_index).map(|s| s.index);
let out_section_index = out_sections.get(section_index as usize).map(|s| s.index);
let index = writer.reserve_symbol_index(out_section_index);
let sym = object::write::elf::Sym {
name: None,
@ -556,19 +564,19 @@ pub fn write_elf(obj: &ObjInfo, export_all: bool) -> Result<Vec<u8>> {
for (symbol_index, symbol) in obj
.symbols
.iter()
.enumerate()
.filter(|&(_, s)| s.flags.is_local())
.chain(obj.symbols.iter().enumerate().filter(|&(_, s)| !s.flags.is_local()))
.chain(obj.symbols.iter().filter(|&(_, s)| !s.flags.is_local()))
{
if obj.kind == ObjKind::Relocatable && symbol.kind == ObjSymbolKind::Section {
// We wrote section symbols above, so skip them here
let section_index =
symbol.section.ok_or_else(|| anyhow!("section symbol without section index"))?;
symbol_map[symbol_index] = Some(section_symbol_offset + section_index as u32);
symbol_map[symbol_index as usize] =
Some(section_symbol_offset as ObjSectionIndex + section_index);
continue;
}
let section = symbol.section.and_then(|idx| out_sections.get(idx));
let section = symbol.section.and_then(|idx| out_sections.get(idx as usize));
let section_index = section.map(|s| s.index);
let index = writer.reserve_symbol_index(section_index);
let name_index = if symbol.name.is_empty() {
@ -612,7 +620,7 @@ pub fn write_elf(obj: &ObjInfo, export_all: bool) -> Result<Vec<u8>> {
num_local = writer.symbol_count();
}
out_symbols.push(OutSymbol { index, sym });
symbol_map[symbol_index] = Some(index.0);
symbol_map[symbol_index as usize] = Some(index.0);
if let Some((comment_data, _)) = &mut comment_data {
CommentSym::from(symbol, export_all).to_writer_static(comment_data, Endian::Big)?;
}
@ -630,7 +638,7 @@ pub fn write_elf(obj: &ObjInfo, export_all: bool) -> Result<Vec<u8>> {
writer.reserve_file_header();
if obj.kind == ObjKind::Executable {
writer.reserve_program_headers(obj.sections.len() as u32);
writer.reserve_program_headers(obj.sections.len());
}
for ((_, section), out_section) in obj.sections.iter().zip(&mut out_sections) {
@ -729,7 +737,7 @@ pub fn write_elf(obj: &ObjInfo, export_all: bool) -> Result<Vec<u8>> {
ensure!(writer.len() == out_section.rela_offset);
for (addr, reloc) in section.relocations.iter() {
let (r_offset, r_type) = reloc.to_elf(addr);
let r_sym = symbol_map[reloc.target_symbol]
let r_sym = symbol_map[reloc.target_symbol as usize]
.ok_or_else(|| anyhow!("Relocation against stripped symbol"))?;
writer.write_relocation(true, &Rel { r_offset, r_sym, r_type, r_addend: reloc.addend });
}
@ -824,9 +832,16 @@ pub fn write_elf(obj: &ObjInfo, export_all: bool) -> Result<Vec<u8>> {
// Write .note.split section header
if let Some((metadata, idx)) = &split_meta {
let out_section = &out_sections[*idx];
let mut sh_type = SHT_SPLITMETA;
if matches!(&obj.mw_comment, Some(comment) if comment.version < 14) {
// Prior to mwld GC 3.0a3, the linker doesn't support ELF .note sections
// properly. With GC 2.7, it crashes if the section type is SHT_NOTE.
// Use the same section type as .mwcats.* so the linker ignores it.
sh_type = SHT_MWCATS;
}
writer.write_section_header(&SectionHeader {
name: Some(out_section.name),
sh_type: SHT_SPLITMETA,
sh_type,
sh_flags: 0,
sh_addr: 0,
sh_offset: out_section.offset as u64,
@ -881,7 +896,7 @@ fn to_obj_symbol(
name: name.to_string(),
demangled_name: demangle(name, &Default::default()),
address: symbol.address(),
section: section_idx,
section: section_idx.map(|s| s as ObjSectionIndex),
size: symbol.size(),
size_known: true,
flags,
@ -915,7 +930,7 @@ pub fn to_obj_reloc_kind(flags: RelocationFlags) -> Result<ObjRelocKind> {
fn to_obj_reloc(
obj_file: &object::File<'_>,
symbol_indexes: &[Option<usize>],
symbol_indexes: &[Option<ObjSymbolIndex>],
section_data: &[u8],
address: u64,
reloc: Relocation,

View File

@ -1,234 +1,48 @@
use std::{
ffi::OsStr,
fs,
fs::{DirBuilder, File, OpenOptions},
io::{BufRead, BufReader, BufWriter, Cursor, Read, Seek, SeekFrom},
path::{Component, Path, PathBuf},
io,
io::{BufRead, BufWriter, Read, Seek, SeekFrom, Write},
};
use anyhow::{anyhow, bail, Context, Result};
use anyhow::{anyhow, Context, Result};
use filetime::{set_file_mtime, FileTime};
use memmap2::{Mmap, MmapOptions};
use path_slash::PathBufExt;
use rarc::RarcReader;
use sha1::{Digest, Sha1};
use typed_path::{Utf8NativePath, Utf8NativePathBuf, Utf8UnixPathBuf};
use xxhash_rust::xxh3::xxh3_64;
use crate::{
array_ref,
util::{
ncompress::{decompress_yay0, decompress_yaz0, YAY0_MAGIC, YAZ0_MAGIC},
rarc,
rarc::{Node, RARC_MAGIC},
take_seek::{TakeSeek, TakeSeekExt},
u8_arc::{U8View, U8_MAGIC},
path::check_path_buf,
Bytes,
},
vfs::{open_file, VfsFile},
};
pub struct MappedFile {
mmap: Mmap,
mtime: FileTime,
offset: u64,
len: u64,
}
impl MappedFile {
pub fn as_reader(&self) -> Cursor<&[u8]> { Cursor::new(self.as_slice()) }
pub fn as_slice(&self) -> &[u8] {
&self.mmap[self.offset as usize..self.offset as usize + self.len as usize]
}
pub fn len(&self) -> u64 { self.len }
pub fn is_empty(&self) -> bool { self.len == 0 }
pub fn into_inner(self) -> Mmap { self.mmap }
}
pub fn split_path<P>(path: P) -> Result<(PathBuf, Option<PathBuf>)>
where P: AsRef<Path> {
let mut base_path = PathBuf::new();
let mut sub_path: Option<PathBuf> = None;
for component in path.as_ref().components() {
if let Component::Normal(str) = component {
let str = str.to_str().ok_or(anyhow!("Path is not valid UTF-8"))?;
if let Some((a, b)) = str.split_once(':') {
base_path.push(a);
sub_path = Some(PathBuf::from(b));
continue;
}
}
if let Some(sub_path) = &mut sub_path {
sub_path.push(component);
} else {
base_path.push(component);
}
}
Ok((base_path, sub_path))
}
/// Opens a memory mapped file, and decompresses it if needed.
pub fn map_file<P>(path: P) -> Result<FileEntry>
where P: AsRef<Path> {
let (base_path, sub_path) = split_path(path.as_ref())?;
let file = File::open(&base_path)
.with_context(|| format!("Failed to open file '{}'", base_path.display()))?;
let mtime = FileTime::from_last_modification_time(&file.metadata()?);
let mmap = unsafe { MmapOptions::new().map(&file) }
.with_context(|| format!("Failed to mmap file: '{}'", base_path.display()))?;
let (offset, len) = if let Some(sub_path) = sub_path {
if sub_path.as_os_str() == OsStr::new("nlzss") {
return Ok(FileEntry::Buffer(
nintendo_lz::decompress(&mut mmap.as_ref())
.map_err(|e| {
anyhow!(
"Failed to decompress '{}' with NLZSS: {}",
path.as_ref().display(),
e
)
})?
.into_boxed_slice(),
mtime,
));
} else if sub_path.as_os_str() == OsStr::new("yaz0") {
return Ok(FileEntry::Buffer(
decompress_yaz0(mmap.as_ref()).with_context(|| {
format!("Failed to decompress '{}' with Yaz0", path.as_ref().display())
})?,
mtime,
));
} else if sub_path.as_os_str() == OsStr::new("yay0") {
return Ok(FileEntry::Buffer(
decompress_yay0(mmap.as_ref()).with_context(|| {
format!("Failed to decompress '{}' with Yay0", path.as_ref().display())
})?,
mtime,
));
}
let buf = mmap.as_ref();
match *array_ref!(buf, 0, 4) {
RARC_MAGIC => {
let rarc = RarcReader::new(&mut Cursor::new(mmap.as_ref())).with_context(|| {
format!("Failed to open '{}' as RARC archive", base_path.display())
})?;
let (offset, size) = rarc.find_file(&sub_path)?.ok_or_else(|| {
anyhow!("File '{}' not found in '{}'", sub_path.display(), base_path.display())
})?;
(offset, size as u64)
}
U8_MAGIC => {
let arc = U8View::new(buf).map_err(|e| {
anyhow!("Failed to open '{}' as U8 archive: {}", base_path.display(), e)
})?;
let (_, node) = arc.find(sub_path.to_slash_lossy().as_ref()).ok_or_else(|| {
anyhow!("File '{}' not found in '{}'", sub_path.display(), base_path.display())
})?;
(node.offset() as u64, node.length() as u64)
}
_ => bail!("Couldn't detect archive type for '{}'", path.as_ref().display()),
}
} else {
(0, mmap.len() as u64)
};
let map = MappedFile { mmap, mtime, offset, len };
let buf = map.as_slice();
// Auto-detect compression if there's a magic number.
if buf.len() > 4 {
match *array_ref!(buf, 0, 4) {
YAZ0_MAGIC => {
return Ok(FileEntry::Buffer(
decompress_yaz0(buf).with_context(|| {
format!("Failed to decompress '{}' with Yaz0", path.as_ref().display())
})?,
mtime,
));
}
YAY0_MAGIC => {
return Ok(FileEntry::Buffer(
decompress_yay0(buf).with_context(|| {
format!("Failed to decompress '{}' with Yay0", path.as_ref().display())
})?,
mtime,
));
}
_ => {}
}
}
Ok(FileEntry::MappedFile(map))
}
/// Opens a memory mapped file without decompression or archive handling.
pub fn map_file_basic<P>(path: P) -> Result<FileEntry>
where P: AsRef<Path> {
let path = path.as_ref();
let file =
File::open(path).with_context(|| format!("Failed to open file '{}'", path.display()))?;
let mtime = FileTime::from_last_modification_time(&file.metadata()?);
let mmap = unsafe { MmapOptions::new().map(&file) }
.with_context(|| format!("Failed to mmap file: '{}'", path.display()))?;
let len = mmap.len() as u64;
Ok(FileEntry::MappedFile(MappedFile { mmap, mtime, offset: 0, len }))
}
pub type OpenedFile = TakeSeek<File>;
/// Opens a file (not memory mapped). No decompression is performed.
pub fn open_file<P>(path: P) -> Result<OpenedFile>
where P: AsRef<Path> {
let (base_path, sub_path) = split_path(path)?;
let mut file = File::open(&base_path)
.with_context(|| format!("Failed to open file '{}'", base_path.display()))?;
let (offset, size) = if let Some(sub_path) = sub_path {
let rarc = RarcReader::new(&mut BufReader::new(&file))
.with_context(|| format!("Failed to read RARC '{}'", base_path.display()))?;
rarc.find_file(&sub_path)?.map(|(o, s)| (o, s as u64)).ok_or_else(|| {
anyhow!("File '{}' not found in '{}'", sub_path.display(), base_path.display())
})?
} else {
(0, file.seek(SeekFrom::End(0))?)
};
file.seek(SeekFrom::Start(offset))?;
Ok(file.take_seek(size))
}
pub trait Reader: BufRead + Seek {}
impl Reader for Cursor<&[u8]> {}
/// Creates a buffered reader around a file (not memory mapped).
pub fn buf_reader<P>(path: P) -> Result<BufReader<File>>
where P: AsRef<Path> {
let file = File::open(&path)
.with_context(|| format!("Failed to open file '{}'", path.as_ref().display()))?;
Ok(BufReader::new(file))
}
/// Creates a buffered writer around a file (not memory mapped).
pub fn buf_writer<P>(path: P) -> Result<BufWriter<File>>
where P: AsRef<Path> {
if let Some(parent) = path.as_ref().parent() {
pub fn buf_writer(path: &Utf8NativePath) -> Result<BufWriter<File>> {
if let Some(parent) = path.parent() {
DirBuilder::new().recursive(true).create(parent)?;
}
let file = File::create(&path)
.with_context(|| format!("Failed to create file '{}'", path.as_ref().display()))?;
let file = File::create(path).with_context(|| format!("Failed to create file '{}'", path))?;
Ok(BufWriter::new(file))
}
/// Reads a string with known size at the specified offset.
pub fn read_string<R>(reader: &mut R, off: u64, size: usize) -> Result<String>
pub fn read_string<R>(reader: &mut R, off: u64, size: usize) -> io::Result<String>
where R: Read + Seek + ?Sized {
let mut data = vec![0u8; size];
let pos = reader.stream_position()?;
reader.seek(SeekFrom::Start(off))?;
reader.read_exact(&mut data)?;
reader.seek(SeekFrom::Start(pos))?;
Ok(String::from_utf8(data)?)
String::from_utf8(data).map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e))
}
/// Reads a zero-terminated string at the specified offset.
pub fn read_c_string<R>(reader: &mut R, off: u64) -> Result<String>
pub fn read_c_string<R>(reader: &mut R, off: u64) -> io::Result<String>
where R: Read + Seek + ?Sized {
let pos = reader.stream_position()?;
reader.seek(SeekFrom::Start(off))?;
@ -246,22 +60,21 @@ where R: Read + Seek + ?Sized {
}
/// Process response files (starting with '@') and glob patterns (*).
pub fn process_rsp(files: &[PathBuf]) -> Result<Vec<PathBuf>> {
let mut out = Vec::with_capacity(files.len());
pub fn process_rsp(files: &[Utf8NativePathBuf]) -> Result<Vec<Utf8NativePathBuf>> {
let mut out = Vec::<Utf8NativePathBuf>::with_capacity(files.len());
for path in files {
let path_str =
path.to_str().ok_or_else(|| anyhow!("'{}' is not valid UTF-8", path.display()))?;
if let Some(rsp_file) = path_str.strip_prefix('@') {
let reader = buf_reader(rsp_file)?;
for result in reader.lines() {
if let Some(rsp_file) = path.as_str().strip_prefix('@') {
let file = open_file(Utf8NativePath::new(rsp_file), true)?;
for result in file.lines() {
let line = result?;
if !line.is_empty() {
out.push(PathBuf::from_slash(line));
out.push(Utf8UnixPathBuf::from(line).with_encoding());
}
}
} else if path_str.contains('*') {
for entry in glob::glob(path_str)? {
out.push(entry?);
} else if path.as_str().contains('*') {
for entry in glob::glob(path.as_str())? {
let path = check_path_buf(entry?)?;
out.push(path.with_encoding());
}
} else {
out.push(path.clone());
@ -270,120 +83,20 @@ pub fn process_rsp(files: &[PathBuf]) -> Result<Vec<PathBuf>> {
Ok(out)
}
/// Iterator over files in a RARC archive.
struct RarcIterator {
file: MappedFile,
base_path: PathBuf,
paths: Vec<(PathBuf, u64, u32)>,
index: usize,
}
impl RarcIterator {
pub fn new(file: MappedFile, base_path: &Path) -> Result<Self> {
let reader = RarcReader::new(&mut file.as_reader())?;
let paths = Self::collect_paths(&reader, base_path);
Ok(Self { file, base_path: base_path.to_owned(), paths, index: 0 })
}
fn collect_paths(reader: &RarcReader, base_path: &Path) -> Vec<(PathBuf, u64, u32)> {
let mut current_path = PathBuf::new();
let mut paths = vec![];
for node in reader.nodes() {
match node {
Node::DirectoryBegin { name } => {
current_path.push(name.name);
}
Node::DirectoryEnd { name: _ } => {
current_path.pop();
}
Node::File { name, offset, size } => {
let path = base_path.join(&current_path).join(name.name);
paths.push((path, offset, size));
}
Node::CurrentDirectory => {}
Node::ParentDirectory => {}
}
}
paths
}
}
impl Iterator for RarcIterator {
type Item = Result<(PathBuf, Box<[u8]>)>;
fn next(&mut self) -> Option<Self::Item> {
if self.index >= self.paths.len() {
return None;
}
let (path, off, size) = self.paths[self.index].clone();
self.index += 1;
let slice = &self.file.as_slice()[off as usize..off as usize + size as usize];
match decompress_if_needed(slice) {
Ok(buf) => Some(Ok((path, buf.into_owned()))),
Err(e) => Some(Err(e)),
}
}
}
/// A file entry, either a memory mapped file or an owned buffer.
pub enum FileEntry {
MappedFile(MappedFile),
Buffer(Box<[u8]>, FileTime),
}
impl FileEntry {
/// Creates a reader for the file.
pub fn as_reader(&self) -> Cursor<&[u8]> {
match self {
Self::MappedFile(file) => file.as_reader(),
Self::Buffer(slice, _) => Cursor::new(slice),
}
}
pub fn as_slice(&self) -> &[u8] {
match self {
Self::MappedFile(file) => file.as_slice(),
Self::Buffer(slice, _) => slice,
}
}
pub fn len(&self) -> u64 {
match self {
Self::MappedFile(file) => file.len(),
Self::Buffer(slice, _) => slice.len() as u64,
}
}
pub fn is_empty(&self) -> bool {
match self {
Self::MappedFile(file) => file.is_empty(),
Self::Buffer(slice, _) => slice.is_empty(),
}
}
pub fn mtime(&self) -> FileTime {
match self {
Self::MappedFile(file) => file.mtime,
Self::Buffer(_, mtime) => *mtime,
}
}
}
/// Information about a file when it was read.
/// Used to determine if a file has changed since it was read (mtime)
/// and if it needs to be written (hash).
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct FileReadInfo {
pub mtime: FileTime,
pub mtime: Option<FileTime>,
pub hash: u64,
}
impl FileReadInfo {
pub fn new(entry: &FileEntry) -> Result<Self> {
let hash = xxh3_64(entry.as_slice());
Ok(Self { mtime: entry.mtime(), hash })
pub fn new(entry: &mut dyn VfsFile) -> Result<Self> {
let hash = xxh3_64(entry.map()?);
let metadata = entry.metadata()?;
Ok(Self { mtime: metadata.mtime, hash })
}
}
@ -391,108 +104,37 @@ impl FileReadInfo {
/// If a file is a RARC archive, iterate over its contents.
/// If a file is a Yaz0 compressed file, decompress it.
pub struct FileIterator {
paths: Vec<PathBuf>,
paths: Vec<Utf8NativePathBuf>,
index: usize,
rarc: Option<RarcIterator>,
}
impl FileIterator {
pub fn new(paths: &[PathBuf]) -> Result<Self> {
Ok(Self { paths: process_rsp(paths)?, index: 0, rarc: None })
pub fn new(paths: &[Utf8NativePathBuf]) -> Result<Self> {
Ok(Self { paths: process_rsp(paths)?, index: 0 })
}
fn next_rarc(&mut self) -> Option<Result<(PathBuf, FileEntry)>> {
if let Some(rarc) = &mut self.rarc {
match rarc.next() {
Some(Ok((path, buf))) => {
let mut path_str = rarc.base_path.as_os_str().to_os_string();
path_str.push(OsStr::new(":"));
path_str.push(path.as_os_str());
return Some(Ok((path, FileEntry::Buffer(buf, rarc.file.mtime))));
}
Some(Err(err)) => return Some(Err(err)),
None => self.rarc = None,
}
}
None
}
fn next_path(&mut self) -> Option<Result<(PathBuf, FileEntry)>> {
fn next_path(&mut self) -> Option<Result<(Utf8NativePathBuf, Box<dyn VfsFile>)>> {
if self.index >= self.paths.len() {
return None;
}
let path = self.paths[self.index].clone();
self.index += 1;
match map_file(&path) {
Ok(FileEntry::MappedFile(map)) => self.handle_file(map, path),
Ok(FileEntry::Buffer(_, _)) => todo!(),
Err(err) => Some(Err(err)),
match open_file(&path, true) {
Ok(file) => Some(Ok((path, file))),
Err(e) => Some(Err(e)),
}
}
fn handle_file(
&mut self,
file: MappedFile,
path: PathBuf,
) -> Option<Result<(PathBuf, FileEntry)>> {
let buf = file.as_slice();
if buf.len() <= 4 {
return Some(Ok((path, FileEntry::MappedFile(file))));
}
match *array_ref!(buf, 0, 4) {
YAZ0_MAGIC => self.handle_yaz0(file, path),
YAY0_MAGIC => self.handle_yay0(file, path),
RARC_MAGIC => self.handle_rarc(file, path),
_ => Some(Ok((path, FileEntry::MappedFile(file)))),
}
}
fn handle_yaz0(
&mut self,
file: MappedFile,
path: PathBuf,
) -> Option<Result<(PathBuf, FileEntry)>> {
Some(match decompress_yaz0(file.as_slice()) {
Ok(buf) => Ok((path, FileEntry::Buffer(buf, file.mtime))),
Err(e) => Err(e),
})
}
fn handle_yay0(
&mut self,
file: MappedFile,
path: PathBuf,
) -> Option<Result<(PathBuf, FileEntry)>> {
Some(match decompress_yay0(file.as_slice()) {
Ok(buf) => Ok((path, FileEntry::Buffer(buf, file.mtime))),
Err(e) => Err(e),
})
}
fn handle_rarc(
&mut self,
file: MappedFile,
path: PathBuf,
) -> Option<Result<(PathBuf, FileEntry)>> {
self.rarc = match RarcIterator::new(file, &path) {
Ok(iter) => Some(iter),
Err(e) => return Some(Err(e)),
};
self.next()
}
}
impl Iterator for FileIterator {
type Item = Result<(PathBuf, FileEntry)>;
type Item = Result<(Utf8NativePathBuf, Box<dyn VfsFile>)>;
fn next(&mut self) -> Option<Self::Item> { self.next_rarc().or_else(|| self.next_path()) }
fn next(&mut self) -> Option<Self::Item> { self.next_path() }
}
pub fn touch<P>(path: P) -> std::io::Result<()>
where P: AsRef<Path> {
if path.as_ref().exists() {
pub fn touch(path: &Utf8NativePath) -> io::Result<()> {
if fs::exists(path)? {
set_file_mtime(path, FileTime::now())
} else {
match OpenOptions::new().create(true).truncate(true).write(true).open(path) {
@ -514,13 +156,16 @@ pub fn decompress_if_needed(buf: &[u8]) -> Result<Bytes> {
}
pub fn verify_hash(buf: &[u8], expected_str: &str) -> Result<()> {
let mut hasher = Sha1::new();
hasher.update(buf);
check_hash_str(hasher.finalize().into(), expected_str)
}
pub fn check_hash_str(hash_bytes: [u8; 20], expected_str: &str) -> Result<()> {
let mut expected_bytes = [0u8; 20];
hex::decode_to_slice(expected_str, &mut expected_bytes)
.with_context(|| format!("Invalid SHA-1 '{expected_str}'"))?;
let mut hasher = Sha1::new();
hasher.update(buf);
let hash_bytes = hasher.finalize();
if hash_bytes.as_ref() == expected_bytes {
if hash_bytes == expected_bytes {
Ok(())
} else {
Err(anyhow!(
@ -530,3 +175,44 @@ pub fn verify_hash(buf: &[u8], expected_str: &str) -> Result<()> {
))
}
}
/// Copies from a buffered reader to a writer without extra allocations.
pub fn buf_copy<R, W>(reader: &mut R, writer: &mut W) -> io::Result<u64>
where
R: BufRead + ?Sized,
W: Write + ?Sized,
{
let mut copied = 0;
loop {
let buf = reader.fill_buf()?;
let len = buf.len();
if len == 0 {
break;
}
writer.write_all(buf)?;
reader.consume(len);
copied += len as u64;
}
Ok(copied)
}
/// Copies from a buffered reader to a writer without extra allocations.
/// Generates an SHA-1 hash of the data as it is copied.
pub fn buf_copy_with_hash<R, W>(reader: &mut R, writer: &mut W) -> io::Result<[u8; 20]>
where
R: BufRead + ?Sized,
W: Write + ?Sized,
{
let mut hasher = Sha1::new();
loop {
let buf = reader.fill_buf()?;
let len = buf.len();
if len == 0 {
break;
}
hasher.update(buf);
writer.write_all(buf)?;
reader.consume(len);
}
Ok(hasher.finalize().into())
}

View File

@ -1,8 +1,6 @@
use std::path::PathBuf;
use anyhow::Result;
use itertools::Itertools;
use path_slash::PathBufExt;
use typed_path::{Utf8NativePathBuf, Utf8UnixPath};
use crate::obj::{ObjInfo, ObjKind};
@ -33,11 +31,11 @@ pub fn generate_ldscript(
let mut force_files = Vec::with_capacity(obj.link_order.len());
for unit in &obj.link_order {
let obj_path = obj_path_for_unit(&unit.name);
force_files.push(obj_path.file_name().unwrap().to_str().unwrap().to_string());
force_files.push(obj_path.file_name().unwrap().to_string());
}
let mut force_active = force_active.to_vec();
for symbol in obj.symbols.iter() {
for (_, symbol) in obj.symbols.iter() {
if symbol.flags.is_exported() && symbol.flags.is_global() && !symbol.flags.is_no_write() {
force_active.push(symbol.name.clone());
}
@ -82,11 +80,11 @@ pub fn generate_ldscript_partial(
let mut force_files = Vec::with_capacity(obj.link_order.len());
for unit in &obj.link_order {
let obj_path = obj_path_for_unit(&unit.name);
force_files.push(obj_path.file_name().unwrap().to_str().unwrap().to_string());
force_files.push(obj_path.file_name().unwrap().to_string());
}
let mut force_active = force_active.to_vec();
for symbol in obj.symbols.iter() {
for (_, symbol) in obj.symbols.iter() {
if symbol.flags.is_exported() && symbol.flags.is_global() && !symbol.flags.is_no_write() {
force_active.push(symbol.name.clone());
}
@ -99,6 +97,10 @@ pub fn generate_ldscript_partial(
Ok(out)
}
pub fn obj_path_for_unit(unit: &str) -> PathBuf { PathBuf::from_slash(unit).with_extension("o") }
pub fn obj_path_for_unit(unit: &str) -> Utf8NativePathBuf {
Utf8UnixPath::new(unit).with_encoding().with_extension("o")
}
pub fn asm_path_for_unit(unit: &str) -> PathBuf { PathBuf::from_slash(unit).with_extension("s") }
pub fn asm_path_for_unit(unit: &str) -> Utf8NativePathBuf {
Utf8UnixPath::new(unit).with_encoding().with_extension("s")
}

View File

@ -5,23 +5,26 @@ use std::{
hash::Hash,
io::BufRead,
mem::{replace, take},
path::Path,
};
use anyhow::{anyhow, bail, Error, Result};
use cwdemangle::{demangle, DemangleOptions};
use flagset::FlagSet;
use indexmap::IndexMap;
use itertools::Itertools;
use multimap::MultiMap;
use once_cell::sync::Lazy;
use regex::{Captures, Regex};
use typed_path::Utf8NativePath;
use crate::{
obj::{
ObjInfo, ObjKind, ObjSplit, ObjSymbol, ObjSymbolFlagSet, ObjSymbolFlags, ObjSymbolKind,
ObjUnit,
section_kind_for_section, ObjArchitecture, ObjInfo, ObjKind, ObjSection, ObjSectionKind,
ObjSections, ObjSplit, ObjSymbol, ObjSymbolFlagSet, ObjSymbolFlags, ObjSymbolKind,
ObjSymbols, ObjUnit, SectionIndex,
},
util::{file::map_file, nested::NestedVec},
util::nested::NestedVec,
vfs::open_file,
};
#[derive(Debug, Copy, Clone, Eq, PartialEq)]
@ -127,7 +130,7 @@ pub struct MapInfo {
pub unit_references: MultiMap<SymbolRef, String>,
pub sections: Vec<SectionInfo>,
pub link_map_symbols: HashMap<SymbolRef, SymbolEntry>,
pub section_symbols: HashMap<String, BTreeMap<u32, Vec<SymbolEntry>>>,
pub section_symbols: IndexMap<String, BTreeMap<u32, Vec<SymbolEntry>>>,
pub section_units: HashMap<String, Vec<(u32, String)>>,
// For common BSS inflation correction
pub common_bss_start: Option<u32>,
@ -711,26 +714,61 @@ where
Ok(sm.result)
}
pub fn apply_map_file<P>(
path: P,
pub fn apply_map_file(
path: &Utf8NativePath,
obj: &mut ObjInfo,
common_bss_start: Option<u32>,
mw_comment_version: Option<u8>,
) -> Result<()>
where
P: AsRef<Path>,
{
let file = map_file(&path)?;
let info = process_map(&mut file.as_reader(), common_bss_start, mw_comment_version)?;
apply_map(&info, obj)
) -> Result<()> {
let mut file = open_file(path, true)?;
let info = process_map(file.as_mut(), common_bss_start, mw_comment_version)?;
apply_map(info, obj)
}
pub fn apply_map(result: &MapInfo, obj: &mut ObjInfo) -> Result<()> {
const DEFAULT_REL_SECTIONS: &[&str] =
&[".init", ".text", ".ctors", ".dtors", ".rodata", ".data", ".bss"];
fn normalize_section_name(name: &str) -> &str {
match name {
".extabindex" => "extabindex",
".extab" => "extab",
_ => name,
}
}
pub fn apply_map(mut result: MapInfo, obj: &mut ObjInfo) -> Result<()> {
if result.sections.is_empty() && obj.kind == ObjKind::Executable {
log::warn!("Memory map section missing, attempting to recreate");
for (section_name, symbol_map) in &result.section_symbols {
let mut address = u32::MAX;
let mut size = 0;
for symbol_entry in symbol_map.values().flatten() {
if symbol_entry.address < address {
address = symbol_entry.address;
}
if symbol_entry.address + symbol_entry.size > address + size {
size = symbol_entry.address + symbol_entry.size - address;
}
}
log::info!("Recreated section {} @ {:#010X} ({:#X})", section_name, address, size);
result.sections.push(SectionInfo {
name: normalize_section_name(section_name).to_string(),
address,
size,
file_offset: 0,
});
}
}
for (section_index, section) in obj.sections.iter_mut() {
let opt = if obj.kind == ObjKind::Executable {
result.sections.iter().find(|s| s.address == section.address as u32)
result.sections.iter().find(|s| {
// Slightly fuzzy match for postprocess/broken maps (TP, SMG)
s.address >= section.address as u32
&& (s.address + s.size) <= (section.address + section.size) as u32
})
} else {
result.sections.iter().filter(|s| s.size > 0).nth(section_index)
result.sections.iter().filter(|s| s.size > 0).nth(section_index as usize)
};
if let Some(info) = opt {
if section.section_known && section.name != info.name {
@ -753,9 +791,172 @@ pub fn apply_map(result: &MapInfo, obj: &mut ObjInfo) -> Result<()> {
section.rename(info.name.clone())?;
} else {
log::warn!("Section {} @ {:#010X} not found in map", section.name, section.address);
if obj.kind == ObjKind::Relocatable {
let new_name = match section.kind {
ObjSectionKind::Code => {
if section.elf_index == 0 {
".init"
} else {
".text"
}
}
ObjSectionKind::Data | ObjSectionKind::ReadOnlyData => {
if section.elf_index == 4 {
if result
.section_symbols
.get(".rodata")
.map_or(false, |m| !m.is_empty())
{
".rodata"
} else {
".data"
}
} else if let Some(section_name) =
DEFAULT_REL_SECTIONS.get(section.elf_index as usize)
{
section_name
} else {
".data"
}
}
ObjSectionKind::Bss => ".bss",
};
log::warn!("Defaulting to {}", new_name);
section.rename(new_name.to_string())?;
}
}
}
// If every symbol the map has alignment 4, it's likely bogus
let bogus_alignment =
result.section_symbols.values().flatten().flat_map(|(_, m)| m).all(|s| s.align == Some(4));
if bogus_alignment {
log::warn!("Bogus alignment detected, ignoring");
}
// Add section symbols
for (section_name, symbol_map) in &result.section_symbols {
if section_name == ".dead" {
continue;
}
let section_name = normalize_section_name(section_name);
let (section_index, _) = obj
.sections
.by_name(section_name)?
.ok_or_else(|| anyhow!("Failed to locate section {section_name} from map"))?;
for symbol_entry in symbol_map.values().flatten() {
add_symbol(obj, symbol_entry, Some(section_index), bogus_alignment)?;
}
}
// Add absolute symbols
// TODO
// for symbol_entry in result.link_map_symbols.values().filter(|s| s.unit.is_none()) {
// add_symbol(obj, symbol_entry, None)?;
// }
// Add splits
for (section_name, unit_order) in &result.section_units {
if section_name == ".dead" {
continue;
}
let section_name = normalize_section_name(section_name);
let (_, section) = obj
.sections
.iter_mut()
.find(|(_, s)| s.name == *section_name)
.ok_or_else(|| anyhow!("Failed to locate section '{}'", section_name))?;
let mut iter = unit_order.iter().peekable();
while let Some((addr, unit)) = iter.next() {
let next = iter
.peek()
.map(|(addr, _)| *addr)
.unwrap_or_else(|| (section.address + section.size) as u32);
let common = section_name == ".bss"
&& matches!(result.common_bss_start, Some(start) if *addr >= start);
let unit = unit.replace(' ', "/");
// Disable mw_comment_version for assembly units
if unit.ends_with(".s") && !obj.link_order.iter().any(|u| u.name == unit) {
obj.link_order.push(ObjUnit {
name: unit.clone(),
autogenerated: false,
comment_version: Some(0),
order: None,
});
}
section.splits.push(*addr, ObjSplit {
unit,
end: next,
align: None,
common,
autogenerated: false,
skip: false,
rename: None,
});
}
}
Ok(())
}
pub fn create_obj(result: &MapInfo) -> Result<ObjInfo> {
let sections = result
.sections
.iter()
.map(|s| {
let name = s.name.clone();
let address = s.address as u64;
let size = s.size as u64;
let file_offset = s.file_offset as u64;
let kind = section_kind_for_section(&name).unwrap_or(ObjSectionKind::ReadOnlyData);
ObjSection {
name,
kind,
address,
size,
data: vec![],
align: 0,
elf_index: 0,
relocations: Default::default(),
virtual_address: None,
file_offset,
section_known: true,
splits: Default::default(),
}
})
.collect();
let mut obj = ObjInfo {
kind: ObjKind::Executable,
architecture: ObjArchitecture::PowerPc,
name: "".to_string(),
symbols: ObjSymbols::new(ObjKind::Executable, vec![]),
sections: ObjSections::new(ObjKind::Executable, sections),
entry: None, // TODO result.entry_point
mw_comment: None,
split_meta: None,
sda2_base: None,
sda_base: None,
stack_address: None,
stack_end: None,
db_stack_addr: None,
arena_lo: None,
arena_hi: None,
link_order: vec![],
blocked_relocation_sources: Default::default(),
blocked_relocation_targets: Default::default(),
known_functions: Default::default(),
module_id: 0,
unresolved_relocations: vec![],
};
// If every symbol the map has alignment 4, it's likely bogus
let bogus_alignment =
result.section_symbols.values().flatten().flat_map(|(_, m)| m).all(|s| s.align == Some(4));
if bogus_alignment {
log::warn!("Bogus alignment detected, ignoring");
}
// Add section symbols
for (section_name, symbol_map) in &result.section_symbols {
let (section_index, _) = obj
@ -763,16 +964,10 @@ pub fn apply_map(result: &MapInfo, obj: &mut ObjInfo) -> Result<()> {
.by_name(section_name)?
.ok_or_else(|| anyhow!("Failed to locate section {section_name} from map"))?;
for symbol_entry in symbol_map.values().flatten() {
add_symbol(obj, symbol_entry, Some(section_index))?;
add_symbol(&mut obj, symbol_entry, Some(section_index), bogus_alignment)?;
}
}
// Add absolute symbols
// TODO
// for symbol_entry in result.link_map_symbols.values().filter(|s| s.unit.is_none()) {
// add_symbol(obj, symbol_entry, None)?;
// }
// Add splits
for (section_name, unit_order) in &result.section_units {
let (_, section) = obj
@ -796,6 +991,7 @@ pub fn apply_map(result: &MapInfo, obj: &mut ObjInfo) -> Result<()> {
name: unit.clone(),
autogenerated: false,
comment_version: Some(0),
order: None,
});
}
@ -810,10 +1006,15 @@ pub fn apply_map(result: &MapInfo, obj: &mut ObjInfo) -> Result<()> {
});
}
}
Ok(())
Ok(obj)
}
fn add_symbol(obj: &mut ObjInfo, symbol_entry: &SymbolEntry, section: Option<usize>) -> Result<()> {
fn add_symbol(
obj: &mut ObjInfo,
symbol_entry: &SymbolEntry,
section: Option<SectionIndex>,
ignore_alignment: bool,
) -> Result<()> {
let demangled_name = demangle(&symbol_entry.name, &DemangleOptions::default());
let mut flags: FlagSet<ObjSymbolFlags> = match symbol_entry.visibility {
SymbolVisibility::Unknown => Default::default(),
@ -843,7 +1044,7 @@ fn add_symbol(obj: &mut ObjInfo, symbol_entry: &SymbolEntry, section: Option<usi
SymbolKind::Section => ObjSymbolKind::Section,
SymbolKind::NoType => ObjSymbolKind::Unknown,
},
align: symbol_entry.align,
align: if ignore_alignment { None } else { symbol_entry.align },
..Default::default()
},
true,

View File

@ -15,6 +15,8 @@ pub mod lcf;
pub mod map;
pub mod ncompress;
pub mod nested;
pub mod nlzss;
pub mod path;
pub mod rarc;
pub mod reader;
pub mod rel;

305
src/util/nlzss.rs Normal file
View File

@ -0,0 +1,305 @@
// BSD 2-Clause License
//
// Copyright (c) 2018, Charlotte D
// All rights reserved.
//
// Redistribution and use in source and binary forms, with or without
// modification, are permitted provided that the following conditions are met:
//
// * Redistributions of source code must retain the above copyright notice, this
// list of conditions and the following disclaimer.
//
// * Redistributions in binary form must reproduce the above copyright notice,
// this list of conditions and the following disclaimer in the documentation
// and/or other materials provided with the distribution.
//
// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
// AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
// IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
// DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
// FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
// DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
// SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
// CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
// OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
//
// Source: https://gitlab.com/DarkKirb/nintendo-lz
// Modified to compile with latest edition, use anyhow::Error, and fix various issues.
use std::io::{Cursor, Read, Write};
use anyhow::{bail, ensure, Result};
use byteorder::{LittleEndian, ReadBytesExt, WriteBytesExt};
/// Decompresses an LZ10/LZ11 compressed file. It returns an error when:
///
/// - The file is not a valid LZ10/LZ11 file
/// - The file is truncated (More data was expected than present)
///
/// # Example
///
/// ```rust,ignore
/// let mut f = File::open("Archive.bin.cmp");
/// let mut decompressed = nintendo_lz::decompress(&mut f).unwrap();
/// ```
pub fn decompress<R>(inp: &mut R) -> Result<Vec<u8>>
where R: Read + ?Sized {
let mut length = inp.read_u32::<LittleEndian>()? as usize;
let ver = match length & 0xFF {
0x10 => 0,
0x11 => 1,
_ => bail!("Invalid magic number"),
};
length >>= 8;
if length == 0 && ver == 1 {
length = inp.read_u32::<LittleEndian>()? as usize;
}
let mut out = Vec::<u8>::with_capacity(length);
while out.len() < length {
let byte = inp.read_u8()?;
for bit_no in (0..8).rev() {
if out.len() >= length {
break;
}
if ((byte >> bit_no) & 1) == 0 {
let data = inp.read_u8()?;
out.push(data);
} else {
let lenmsb = inp.read_u8()? as usize;
let lsb = inp.read_u8()? as usize;
let mut length: usize = lenmsb >> 4;
let mut disp: usize = ((lenmsb & 15) << 8) + lsb;
if ver == 0 {
length += 3;
} else if length > 1 {
length += 1;
} else if length == 0 {
length = (lenmsb & 15) << 4;
length += lsb >> 4;
length += 0x11;
let msb = inp.read_u8()? as usize;
disp = ((lsb & 15) << 8) + msb;
} else {
length = (lenmsb & 15) << 12;
length += lsb << 4;
let byte1 = inp.read_u8()? as usize;
let byte2 = inp.read_u8()? as usize;
length += byte1 >> 4;
length += 0x111;
disp = ((byte1 & 15) << 8) + byte2;
}
let start: usize = out.len() - disp - 1;
for i in 0..length {
let val = out[start + i];
out.push(val);
}
}
}
}
Ok(out)
}
/// This function is a convenience wrapper around `decompress` for decompressing slices, arrays or
/// vectors.
pub fn decompress_arr(input: &[u8]) -> Result<Vec<u8>> {
let mut reader = Cursor::new(input);
decompress(&mut reader)
}
/// This enum contains the possible compression levels for LZ compression.
pub enum CompressionLevel {
/// LZ10 compression. Maximum repeat size: 18 bytes
LZ10,
/// LZ11 compression. Maximum repeat size: 65809 bytes
///
/// Argument: Maximum repeat size (0..65810), lower means worse compression but higher speed.
/// for values < 3 compression is disabled
LZ11(u32),
}
fn find_longest_match(data: &[u8], off: usize, max: usize) -> Option<(usize, usize)> {
if off < 4 || data.len() - off < 4 {
return None;
}
let mut longest_pos: usize = 0;
let mut longest_len: usize = 0;
let mut start = 0;
if off > 0x1000 {
start = off - 0x1000;
}
for pos in search(&data[start..off + 2], &data[off..off + 3]) {
let mut length = 0;
for (i, p) in (off..data.len()).enumerate() {
if length == max {
return Some((start + pos, length));
}
if data[p] != data[start + pos + i] {
break;
}
length += 1;
}
if length > longest_len {
longest_pos = pos;
longest_len = length;
}
}
if longest_len < 3 {
return None;
}
Some((start + longest_pos, longest_len))
}
/// Compresses data to LZ10/LZ11. It returns an error when:
///
/// - The input is too large for the selected LZ version (LZ10 supports at most 16MiB)
/// - The maximum repeat length is out of range (for LZ11, has to be in the range (0..65810)
/// - Writing to the output file failed
///
/// # Example
///
/// ```rust,ignore
/// let mut f = File::create("Archive.bin.cmp");
/// let data = b"This is an example text. This is an example text";
/// nintendo_lz::compress(&data, &mut f, nintendo_lz::CompressionLevel::LZ11(65809)).unwrap();
/// ```
pub fn compress<W>(inp: &[u8], out: &mut W, level: CompressionLevel) -> Result<()>
where W: Write + ?Sized {
let ver = match level {
CompressionLevel::LZ10 => 0,
CompressionLevel::LZ11(_) => 1,
};
if ver == 0 && inp.len() > 16777216 {
bail!("Input data too large for LZ10");
}
if ver == 1 && inp.len() as u64 > 0xFFFFFFFF {
bail!("Input data too large for LZ11");
}
let repeat_size = match level {
CompressionLevel::LZ10 => 18,
CompressionLevel::LZ11(max) => max,
};
ensure!(repeat_size < 65810, "Maximum repeat size out of range. (0..65810)");
let size: usize = inp.len();
if size < 16777216 && (size != 0 || ver == 0) {
let header = 0x10 + ver + ((size as u32) << 8);
out.write_u32::<LittleEndian>(header)?;
} else {
out.write_u32::<LittleEndian>(0x11)?;
out.write_u32::<LittleEndian>(size as u32)?;
}
let mut off: usize = 0;
let mut byte: u8 = 0;
let mut index = 7;
let mut cmpbuf: Vec<u8> = Vec::new();
while off < size {
match find_longest_match(inp, off, repeat_size as usize) {
None => {
index -= 1;
cmpbuf.push(inp[off]);
off += 1;
}
Some((pos, len)) => {
let lz_off: usize = off - pos - 1;
byte |= 1 << index;
index -= 1;
if ver == 0 {
let l = len - 3;
let cmp: [u8; 2] = [((lz_off >> 8) as u8) + ((l << 4) as u8), lz_off as u8];
cmpbuf.extend_from_slice(&cmp);
} else if len < 0x11 {
let l = len - 1;
let cmp: [u8; 2] = [((lz_off >> 8) as u8) + ((l << 4) as u8), lz_off as u8];
cmpbuf.extend_from_slice(&cmp);
} else if len < 0x111 {
let l = len - 0x11;
let cmp: [u8; 3] =
[(l >> 4) as u8, ((lz_off >> 8) as u8) + ((l << 4) as u8), lz_off as u8];
cmpbuf.extend_from_slice(&cmp);
} else {
let l = len - 0x111;
let cmp: [u8; 4] = [
(l >> 12) as u8 + 0x10,
(l >> 4) as u8,
((lz_off >> 8) as u8) + ((l << 4) as u8),
lz_off as u8,
];
cmpbuf.extend_from_slice(&cmp);
}
off += len;
}
};
if index < 0 {
out.write_u8(byte)?;
out.write_all(&cmpbuf)?;
byte = 0;
index = 7;
cmpbuf.clear();
}
}
if !cmpbuf.is_empty() {
out.write_u8(byte)?;
out.write_all(&cmpbuf)?;
}
out.write_u8(0xFF)?;
Ok(())
}
/// This function is a convenience wrapper around `compress` for compressing to a Vec<u8>.
/// Additionally, it uses LZ11 as compression algorithm by default.
pub fn compress_arr(input: &[u8]) -> Result<Vec<u8>> {
let mut out: Vec<u8> = Vec::new();
{
let mut writer = Cursor::new(&mut out);
compress(input, &mut writer, CompressionLevel::LZ11(65809))?;
}
Ok(out)
}
fn get_needle_table(needle: &[u8]) -> [usize; 256] {
let mut needle_table = [needle.len(); 256];
for (i, c) in needle.iter().enumerate() {
needle_table[*c as usize] = needle.len() - i;
}
needle_table
}
pub fn search_one(haystack: &[u8], needle: &[u8], needle_table: &[usize; 256]) -> Option<usize> {
let mut cur = 0;
while haystack.len() - cur >= needle.len() {
let mut output = None;
for i in (0..needle.len()).rev() {
if haystack[cur + i] == needle[i] {
output = Some(cur);
break;
}
}
if output.is_some() {
return output;
}
cur += needle_table[haystack[cur + needle.len() - 1] as usize];
}
None
}
fn search(haystack: &[u8], needle: &[u8]) -> Vec<usize> {
let needle_table = get_needle_table(needle);
let mut cur = 0usize;
let mut positions = Vec::new();
while cur + needle.len() < haystack.len() {
let found_pos = search_one(&haystack[cur..], needle, &needle_table);
if let Some(pos) = found_pos {
positions.push(pos);
cur += pos + needle.len() + 1;
} else {
return positions;
}
}
positions
}

26
src/util/path.rs Normal file
View File

@ -0,0 +1,26 @@
use std::{
path::{Path, PathBuf},
str::Utf8Error,
string::FromUtf8Error,
};
use typed_path::{NativePath, NativePathBuf, Utf8NativePath, Utf8NativePathBuf};
// For argp::FromArgs
pub fn native_path(value: &str) -> Result<Utf8NativePathBuf, String> {
Ok(Utf8NativePathBuf::from(value))
}
/// Checks if the path is valid UTF-8 and returns it as a [`Utf8NativePath`].
#[inline]
pub fn check_path(path: &Path) -> Result<&Utf8NativePath, Utf8Error> {
Utf8NativePath::from_bytes_path(NativePath::new(path.as_os_str().as_encoded_bytes()))
}
/// Checks if the path is valid UTF-8 and returns it as a [`Utf8NativePathBuf`].
#[inline]
pub fn check_path_buf(path: PathBuf) -> Result<Utf8NativePathBuf, FromUtf8Error> {
Utf8NativePathBuf::from_bytes_path_buf(NativePathBuf::from(
path.into_os_string().into_encoded_bytes(),
))
}

View File

@ -1,425 +1,283 @@
// Source: https://github.com/Julgodis/picori/blob/650da9f4fe6050b39b80d5360416591c748058d5/src/rarc.rs
// License: MIT
// Modified to use `std::io::Cursor<&[u8]>` and project's FromReader trait
use std::{
collections::HashMap,
fmt::Display,
hash::{Hash, Hasher},
io,
io::{Read, Seek, SeekFrom},
path::{Component, Path, PathBuf},
};
use std::{borrow::Cow, ffi::CStr};
use anyhow::{anyhow, bail, ensure, Result};
use typed_path::Utf8UnixPath;
use zerocopy::{big_endian::*, FromBytes, Immutable, IntoBytes, KnownLayout};
use crate::util::{
file::read_c_string,
reader::{struct_size, Endian, FromReader},
};
#[derive(Debug, Clone)]
pub struct NamedHash {
pub name: String,
pub hash: u16,
}
impl Display for NamedHash {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", self.name)
}
}
impl Hash for NamedHash {
fn hash<H>(&self, state: &mut H)
where H: Hasher {
self.hash.hash(state);
}
}
impl PartialEq for NamedHash {
fn eq(&self, other: &Self) -> bool {
if self.hash == other.hash {
self.name == other.name
} else {
false
}
}
}
impl Eq for NamedHash {}
#[derive(Debug, Clone)]
enum RarcDirectory {
File {
/// Name of the file.
name: NamedHash,
/// Offset of the file in the RARC file. This offset is relative to the start of the RARC file.
offset: u64,
/// Size of the file.
size: u32,
},
Folder {
/// Name of the folder.
name: NamedHash,
},
CurrentFolder,
ParentFolder,
}
#[derive(Debug, Clone)]
struct RarcNode {
/// Index of first directory.
pub index: u32,
/// Number of directories.
pub count: u32,
}
pub struct RarcReader {
directories: Vec<RarcDirectory>,
nodes: HashMap<NamedHash, RarcNode>,
root_node: NamedHash,
}
use crate::{static_assert, vfs::next_non_empty};
pub const RARC_MAGIC: [u8; 4] = *b"RARC";
struct RarcHeader {
#[derive(Copy, Clone, Debug, PartialEq, FromBytes, IntoBytes, Immutable, KnownLayout)]
#[repr(C, align(4))]
pub struct RarcHeader {
/// Magic identifier. (Always "RARC")
magic: [u8; 4],
_file_length: u32,
header_length: u32,
file_offset: u32,
_file_length_2: u32,
_unk0: u32,
_unk1: u32,
_unk2: u32,
node_count: u32,
node_offset: u32,
directory_count: u32,
directory_offset: u32,
string_table_length: u32,
string_table_offset: u32,
_file_count: u16,
_unk3: u16,
_unk4: u32,
/// Length of the RARC file.
file_len: U32,
/// Length of the header. (Always 32)
header_len: U32,
/// Start of the file data, relative to the end of the file header.
data_offset: U32,
/// Length of the file data.
data_len: U32,
_unk1: U32,
_unk2: U32,
_unk3: U32,
}
impl FromReader for RarcHeader {
type Args = ();
static_assert!(size_of::<RarcHeader>() == 0x20);
const STATIC_SIZE: usize = struct_size([
4, // magic
u32::STATIC_SIZE, // file_length
u32::STATIC_SIZE, // header_length
u32::STATIC_SIZE, // file_offset
u32::STATIC_SIZE, // file_length
u32::STATIC_SIZE, // unk0
u32::STATIC_SIZE, // unk1
u32::STATIC_SIZE, // unk2
u32::STATIC_SIZE, // node_count
u32::STATIC_SIZE, // node_offset
u32::STATIC_SIZE, // directory_count
u32::STATIC_SIZE, // directory_offset
u32::STATIC_SIZE, // string_table_length
u32::STATIC_SIZE, // string_table_offset
u16::STATIC_SIZE, // file_count
u16::STATIC_SIZE, // unk3
u32::STATIC_SIZE, // unk4
]);
impl RarcHeader {
/// Length of the RARC file.
pub fn file_len(&self) -> u32 { self.file_len.get() }
fn from_reader_args<R>(reader: &mut R, e: Endian, _args: Self::Args) -> io::Result<Self>
where R: Read + Seek + ?Sized {
let header = Self {
magic: <[u8; 4]>::from_reader(reader, e)?,
_file_length: u32::from_reader(reader, e)?,
header_length: u32::from_reader(reader, e)?,
file_offset: u32::from_reader(reader, e)?,
_file_length_2: u32::from_reader(reader, e)?,
_unk0: u32::from_reader(reader, e)?,
_unk1: u32::from_reader(reader, e)?,
_unk2: u32::from_reader(reader, e)?,
node_count: u32::from_reader(reader, e)?,
node_offset: u32::from_reader(reader, e)?,
directory_count: u32::from_reader(reader, e)?,
directory_offset: u32::from_reader(reader, e)?,
string_table_length: u32::from_reader(reader, e)?,
string_table_offset: u32::from_reader(reader, e)?,
_file_count: u16::from_reader(reader, e)?,
_unk3: u16::from_reader(reader, e)?,
_unk4: u32::from_reader(reader, e)?,
/// Length of the header.
pub fn header_len(&self) -> u32 { self.header_len.get() }
/// Start of the file data, relative to the end of the file header.
pub fn data_offset(&self) -> u32 { self.data_offset.get() }
/// Length of the file data.
pub fn data_len(&self) -> u32 { self.data_len.get() }
}
#[derive(Copy, Clone, Debug, PartialEq, FromBytes, IntoBytes, Immutable, KnownLayout)]
#[repr(C, align(4))]
struct RarcInfo {
/// Number of directories in the directory table.
directory_count: U32,
/// Offset to the start of the directory table, relative to the end of the file header.
directory_offset: U32,
/// Number of nodes in the node table.
node_count: U32,
/// Offset to the start of the node table, relative to the end of the file header.
node_offset: U32,
/// Length of the string table.
string_table_len: U32,
/// Offset to the start of the string table, relative to the end of the file header.
string_table_offset: U32,
/// Number of files in the node table.
_file_count: U16,
_unk4: U16,
_unk5: U32,
}
static_assert!(size_of::<RarcInfo>() == 0x20);
#[derive(Copy, Clone, Debug, PartialEq, FromBytes, IntoBytes, Immutable, KnownLayout)]
#[repr(C, align(4))]
pub struct RarcNode {
/// Index of the node. (0xFFFF for directories)
index: U16,
/// Hash of the node name.
name_hash: U16,
/// Unknown. (0x200 for folders, 0x1100 for files)
_unk0: U16,
/// Offset in the string table to the node name.
name_offset: U16,
/// Files: Offset in the data to the file data.
/// Directories: Index of the directory in the directory table.
data_offset: U32,
/// Files: Length of the data.
/// Directories: Unknown. Always 16.
data_length: U32,
_unk1: U32,
}
static_assert!(size_of::<RarcNode>() == 0x14);
impl RarcNode {
/// Whether the node is a file.
pub fn is_file(&self) -> bool { self.index.get() != 0xFFFF }
/// Whether the node is a directory.
pub fn is_dir(&self) -> bool { self.index.get() == 0xFFFF }
/// Offset in the string table to the node name.
pub fn name_offset(&self) -> u32 { self.name_offset.get() as u32 }
/// Files: Offset in the data to the file data.
/// Directories: Index of the directory in the directory table.
pub fn data_offset(&self) -> u32 { self.data_offset.get() }
/// Files: Length of the data.
/// Directories: Unknown. Always 16.
pub fn data_length(&self) -> u32 { self.data_length.get() }
}
#[derive(Copy, Clone, Debug, PartialEq, FromBytes, IntoBytes, Immutable, KnownLayout)]
#[repr(C, align(4))]
pub struct RarcDirectory {
/// Identifier of the directory.
identifier: [u8; 4],
/// Offset in the string table to the directory name.
name_offset: U32,
/// Hash of the directory name.
name_hash: U16,
/// Number of nodes in the directory.
count: U16,
/// Index of the first node in the directory.
index: U32,
}
static_assert!(size_of::<RarcDirectory>() == 0x10);
impl RarcDirectory {
/// Offset in the string table to the directory name.
pub fn name_offset(&self) -> u32 { self.name_offset.get() }
/// Index of the first node in the directory.
pub fn node_index(&self) -> u32 { self.index.get() }
/// Number of nodes in the directory.
pub fn node_count(&self) -> u16 { self.count.get() }
}
/// A view into a RARC archive.
pub struct RarcView<'a> {
/// The RARC archive header.
pub header: &'a RarcHeader,
/// The directories in the RARC archive.
pub directories: &'a [RarcDirectory],
/// The nodes in the RARC archive.
pub nodes: &'a [RarcNode],
/// The string table containing all file and directory names.
pub string_table: &'a [u8],
/// The file data.
pub data: &'a [u8],
}
impl<'a> RarcView<'a> {
/// Create a new RARC view from a buffer.
pub fn new(buf: &'a [u8]) -> Result<Self, &'static str> {
let Ok((header, remaining)) = RarcHeader::ref_from_prefix(buf) else {
return Err("Buffer not large enough for RARC header");
};
if header.magic != RARC_MAGIC {
return Err(io::Error::new(
io::ErrorKind::InvalidData,
format!("invalid RARC magic: {:?}", header.magic),
));
return Err("RARC magic mismatch");
}
if header.node_count >= 0x10000 {
return Err(io::Error::new(
io::ErrorKind::InvalidData,
format!("invalid node count: {}", header.node_count),
));
if header.header_len.get() as usize != size_of::<RarcHeader>() {
return Err("RARC header size mismatch");
}
if header.directory_count >= 0x10000 {
return Err(io::Error::new(
io::ErrorKind::InvalidData,
format!("invalid directory count: {}", header.directory_count),
));
// All offsets are relative to the _end_ of the header, so we can
// just trim the header from the buffer and use the offsets as is.
let Ok((info, _)) = RarcInfo::ref_from_prefix(remaining) else {
return Err("Buffer not large enough for RARC info");
};
let directory_table_offset = info.directory_offset.get() as usize;
let directory_table_size = info.directory_count.get() as usize * size_of::<RarcDirectory>();
let directories_buf = remaining
.get(directory_table_offset..directory_table_offset + directory_table_size)
.ok_or("RARC directory table out of bounds")?;
let directories = <[RarcDirectory]>::ref_from_bytes(directories_buf)
.map_err(|_| "RARC directory table not aligned")?;
if directories.is_empty() || directories[0].identifier != *b"ROOT" {
return Err("RARC root directory not found");
}
Ok(header)
let node_table_offset = info.node_offset.get() as usize;
let node_table_size = info.node_count.get() as usize * size_of::<RarcNode>();
let nodes_buf = remaining
.get(node_table_offset..node_table_offset + node_table_size)
.ok_or("RARC node table out of bounds")?;
let nodes =
<[RarcNode]>::ref_from_bytes(nodes_buf).map_err(|_| "RARC node table not aligned")?;
let string_table_offset = info.string_table_offset.get() as usize;
let string_table_size = info.string_table_len.get() as usize;
let string_table = remaining
.get(string_table_offset..string_table_offset + string_table_size)
.ok_or("RARC string table out of bounds")?;
let data_offset = header.data_offset.get() as usize;
let data_size = header.data_len.get() as usize;
let data =
buf.get(data_offset..data_offset + data_size).ok_or("RARC file data out of bounds")?;
Ok(Self { header, directories, nodes, string_table, data })
}
}
struct RarcFileNode {
index: u16,
name_hash: u16,
_unk0: u16, // 0x200 for folders, 0x1100 for files
name_offset: u16,
data_offset: u32,
data_length: u32,
_unk1: u32,
}
impl FromReader for RarcFileNode {
type Args = ();
const STATIC_SIZE: usize = struct_size([
u16::STATIC_SIZE, // index
u16::STATIC_SIZE, // name_hash
u16::STATIC_SIZE, // unk0
u16::STATIC_SIZE, // name_offset
u32::STATIC_SIZE, // data_offset
u32::STATIC_SIZE, // data_length
u32::STATIC_SIZE, // unk1
]);
fn from_reader_args<R>(reader: &mut R, e: Endian, _args: Self::Args) -> io::Result<Self>
where R: Read + Seek + ?Sized {
Ok(Self {
index: u16::from_reader(reader, e)?,
name_hash: u16::from_reader(reader, e)?,
_unk0: u16::from_reader(reader, e)?,
name_offset: u16::from_reader(reader, e)?,
data_offset: u32::from_reader(reader, e)?,
data_length: u32::from_reader(reader, e)?,
_unk1: u32::from_reader(reader, e)?,
})
/// Get a string from the string table at the given offset.
pub fn get_string(&self, offset: u32) -> Result<Cow<str>, String> {
let name_buf = self.string_table.get(offset as usize..).ok_or_else(|| {
format!(
"RARC: name offset {} out of bounds (string table size: {})",
offset,
self.string_table.len()
)
})?;
let c_string = CStr::from_bytes_until_nul(name_buf)
.map_err(|_| format!("RARC: name at offset {} not null-terminated", offset))?;
Ok(c_string.to_string_lossy())
}
}
struct RarcDirectoryNode {
_identifier: u32,
name_offset: u32,
name_hash: u16,
count: u16,
index: u32,
}
impl FromReader for RarcDirectoryNode {
type Args = ();
const STATIC_SIZE: usize = struct_size([
u32::STATIC_SIZE, // identifier
u32::STATIC_SIZE, // name_offset
u16::STATIC_SIZE, // name_hash
u16::STATIC_SIZE, // count
u32::STATIC_SIZE, // index
]);
fn from_reader_args<R>(reader: &mut R, e: Endian, _args: Self::Args) -> io::Result<Self>
where R: Read + Seek + ?Sized {
Ok(Self {
_identifier: u32::from_reader(reader, e)?,
name_offset: u32::from_reader(reader, e)?,
name_hash: u16::from_reader(reader, e)?,
count: u16::from_reader(reader, e)?,
index: u32::from_reader(reader, e)?,
})
/// Get the data for a file node.
pub fn get_data(&self, node: RarcNode) -> Result<&[u8], &'static str> {
if node.is_dir() {
return Err("Cannot get data for a directory node");
}
let offset = node.data_offset.get() as usize;
let size = node.data_length.get() as usize;
self.data.get(offset..offset + size).ok_or("RARC file data out of bounds")
}
}
impl RarcReader {
/// Creates a new RARC reader.
pub fn new<R>(reader: &mut R) -> Result<Self>
where R: Read + Seek + ?Sized {
let base = reader.stream_position()?;
let header = RarcHeader::from_reader(reader, Endian::Big)?;
/// Finds a particular file or directory by path.
pub fn find(&self, path: &Utf8UnixPath) -> Option<RarcNodeKind> {
let mut split = path.as_str().split('/');
let mut current = next_non_empty(&mut split);
let base = base + header.header_length as u64;
let directory_base = base + header.directory_offset as u64;
let data_base = base + header.file_offset as u64;
let mut directories = Vec::with_capacity(header.directory_count as usize);
for i in 0..header.directory_count {
reader.seek(SeekFrom::Start(directory_base + 20 * i as u64))?;
let node = RarcFileNode::from_reader(reader, Endian::Big)?;
let name = {
let offset = header.string_table_offset as u64;
let offset = offset + node.name_offset as u64;
ensure!(
(node.name_offset as u32) < header.string_table_length,
"invalid string table offset"
);
read_c_string(reader, base + offset)
}?;
if node.index == 0xFFFF {
if name == "." {
directories.push(RarcDirectory::CurrentFolder);
} else if name == ".." {
directories.push(RarcDirectory::ParentFolder);
} else {
directories.push(RarcDirectory::Folder {
name: NamedHash { name, hash: node.name_hash },
});
}
} else {
directories.push(RarcDirectory::File {
name: NamedHash { name, hash: node.name_hash },
offset: data_base + node.data_offset as u64,
size: node.data_length,
});
let mut dir_idx = 0;
let mut dir = self.directories[dir_idx];
// Allow matching the root directory by name optionally
if let Ok(root_name) = self.get_string(dir.name_offset()) {
if root_name.eq_ignore_ascii_case(current) {
current = next_non_empty(&mut split);
}
}
let node_base = base + header.node_offset as u64;
let mut root_node: Option<NamedHash> = None;
let mut nodes = HashMap::with_capacity(header.node_count as usize);
for i in 0..header.node_count {
reader.seek(SeekFrom::Start(node_base + 16 * i as u64))?;
let node = RarcDirectoryNode::from_reader(reader, Endian::Big)?;
ensure!(node.index < header.directory_count, "first directory index out of bounds");
let last_index = node.index.checked_add(node.count as u32);
ensure!(
last_index.is_some() && last_index.unwrap() <= header.directory_count,
"last directory index out of bounds"
);
let name = {
let offset = header.string_table_offset as u64;
let offset = offset + node.name_offset as u64;
ensure!(
node.name_offset < header.string_table_length,
"invalid string table offset"
);
read_c_string(reader, base + offset)
}?;
// FIXME: this assumes that the root node is the first node in the list
if root_node.is_none() {
root_node = Some(NamedHash { name: name.clone(), hash: node.name_hash });
}
let name = NamedHash { name, hash: node.name_hash };
nodes.insert(name.clone(), RarcNode { index: node.index, count: node.count as u32 });
if current.is_empty() {
return Some(RarcNodeKind::Directory(dir_idx, dir));
}
if let Some(root_node) = root_node {
Ok(Self { directories, nodes, root_node })
} else {
Err(anyhow!("no root node"))
}
}
/// Get a iterator over the nodes in the RARC file.
pub fn nodes(&self) -> Nodes<'_> {
let root_node = self.root_node.clone();
Nodes { parent: self, stack: vec![NodeState::Begin(root_node)] }
}
/// Find a file in the RARC file.
pub fn find_file<P>(&self, path: P) -> Result<Option<(u64, u32)>>
where P: AsRef<Path> {
let mut cmp_path = PathBuf::new();
for component in path.as_ref().components() {
match component {
Component::Normal(name) => cmp_path.push(name.to_ascii_lowercase()),
Component::RootDir => {}
component => bail!("Invalid path component: {:?}", component),
}
}
let mut current_path = PathBuf::new();
for node in self.nodes() {
match node {
Node::DirectoryBegin { name } => {
current_path.push(name.name.to_ascii_lowercase());
}
Node::DirectoryEnd { name: _ } => {
current_path.pop();
}
Node::File { name, offset, size } => {
if current_path.join(name.name.to_ascii_lowercase()) == cmp_path {
return Ok(Some((offset, size)));
}
}
Node::CurrentDirectory => {}
Node::ParentDirectory => {}
}
}
Ok(None)
}
}
/// A node in an RARC file.
pub enum Node {
/// A directory that has been entered.
DirectoryBegin { name: NamedHash },
/// A directory that has been exited.
DirectoryEnd { name: NamedHash },
/// A file in the current directory.
File { name: NamedHash, offset: u64, size: u32 },
/// The current directory. This is equivalent to ".".
CurrentDirectory,
/// The parent directory. This is equivalent to "..".
ParentDirectory,
}
enum NodeState {
Begin(NamedHash),
End(NamedHash),
File(NamedHash, u32),
}
/// An iterator over the nodes in an RARC file.
pub struct Nodes<'parent> {
parent: &'parent RarcReader,
stack: Vec<NodeState>,
}
impl<'parent> Iterator for Nodes<'parent> {
type Item = Node;
fn next(&mut self) -> Option<Self::Item> {
match self.stack.pop()? {
NodeState::Begin(name) => {
self.stack.push(NodeState::File(name.clone(), 0));
Some(Node::DirectoryBegin { name })
}
NodeState::End(name) => Some(Node::DirectoryEnd { name }),
NodeState::File(name, index) => {
if let Some(node) = self.parent.nodes.get(&name) {
if index + 1 >= node.count {
self.stack.push(NodeState::End(name.clone()));
let mut idx = dir.index.get() as usize;
while idx < dir.index.get() as usize + dir.count.get() as usize {
let node = self.nodes.get(idx).copied()?;
let Ok(name) = self.get_string(node.name_offset()) else {
idx += 1;
continue;
};
if name.eq_ignore_ascii_case(current) {
current = next_non_empty(&mut split);
if node.is_dir() {
dir_idx = node.data_offset.get() as usize;
dir = self.directories.get(dir_idx).cloned()?;
idx = dir.index.get() as usize;
if current.is_empty() {
return Some(RarcNodeKind::Directory(dir_idx, dir));
} else {
self.stack.push(NodeState::File(name.clone(), index + 1));
}
let directory = &self.parent.directories[(node.index + index) as usize];
match directory {
RarcDirectory::CurrentFolder => Some(Node::CurrentDirectory),
RarcDirectory::ParentFolder => Some(Node::ParentDirectory),
RarcDirectory::Folder { name } => {
self.stack.push(NodeState::Begin(name.clone()));
self.next()
}
RarcDirectory::File { name, offset, size } => {
Some(Node::File { name: name.clone(), offset: *offset, size: *size })
}
continue;
}
} else {
None
return Some(RarcNodeKind::File(idx, node));
}
}
idx += 1;
}
None
}
/// Get the children of a directory.
pub fn children(&self, dir: RarcDirectory) -> &[RarcNode] {
let start = dir.node_index() as usize;
let end = start + dir.node_count() as usize;
self.nodes.get(start..end).unwrap_or(&[])
}
}
#[derive(Debug)]
pub enum RarcNodeKind {
File(usize, RarcNode),
Directory(usize, RarcDirectory),
}

View File

@ -13,7 +13,7 @@ use crate::{
array_ref_mut,
obj::{
ObjArchitecture, ObjInfo, ObjKind, ObjRelocKind, ObjSection, ObjSectionKind, ObjSymbol,
ObjSymbolFlagSet, ObjSymbolFlags, ObjSymbolKind,
ObjSymbolFlagSet, ObjSymbolFlags, ObjSymbolKind, SectionIndex,
},
util::{
align_up,
@ -418,7 +418,7 @@ where R: Read + Seek + ?Sized {
_ => None, // determined later
}
.unwrap_or_default() as u64,
elf_index: idx,
elf_index: idx as SectionIndex,
relocations: Default::default(),
virtual_address: None, // TODO option to set?
file_offset: offset as u64,
@ -440,7 +440,7 @@ where R: Read + Seek + ?Sized {
let (section_index, _) = sections
.iter()
.enumerate()
.find(|&(_, section)| section.elf_index == rel_section_idx as usize)
.find(|&(_, section)| section.elf_index == rel_section_idx as SectionIndex)
.ok_or_else(|| anyhow!("Failed to locate {name} section {rel_section_idx}"))?;
log::debug!("Adding {name} section {rel_section_idx} offset {offset:#X}");
let mut flags = ObjSymbolFlagSet(ObjSymbolFlags::Global.into());
@ -450,7 +450,7 @@ where R: Read + Seek + ?Sized {
symbols.push(ObjSymbol {
name: name.to_string(),
address: offset as u64,
section: Some(section_index),
section: Some(section_index as SectionIndex),
flags,
kind: ObjSymbolKind::Function,
..Default::default()
@ -853,8 +853,10 @@ where
offset = (offset + align) & !align;
offset += section.size() as u32;
}
// Align to 4 after section data
offset = (offset + 3) & !3;
if info.version >= 3 {
// Align to 4 after section data
offset = (offset + 3) & !3;
}
fn do_relocation_layout(
relocations: &[RelReloc],
@ -1047,8 +1049,8 @@ where
}
w.write_all(&section_data)?;
}
// Align to 4 after section data
{
if info.version >= 3 {
// Align to 4 after section data
let position = w.stream_position()?;
w.write_all(&vec![0u8; calculate_padding(position, 4) as usize])?;
}

View File

@ -9,7 +9,7 @@ use cwdemangle::{demangle, DemangleOptions};
use crate::{
obj::{
ObjArchitecture, ObjInfo, ObjKind, ObjSection, ObjSectionKind, ObjSymbol, ObjSymbolFlagSet,
ObjSymbolFlags, ObjSymbolKind,
ObjSymbolFlags, ObjSymbolKind, SectionIndex,
},
util::{
file::{read_c_string, read_string},
@ -34,11 +34,16 @@ pub const DOL_SECTION_NAMES: [Option<&str>; 14] = [
Some(".sbss2"),
None, // s_zero2
];
pub const RSO_SECTION_NAMES: [&str; 7] =
[".init", ".text", ".ctors", ".dtors", ".rodata", ".data", ".bss"];
/// extabindex section index.
pub const DOL_SECTION_ETI: u32 = 241;
/// ABS symbol section index.
pub const DOL_SECTION_ABS: u32 = 65521;
#[derive(Default)]
pub struct RsoHeader {
// Pointer to the next module, forming a linked list. Always 0, filled in at runtime.
// pub next: u32,
@ -101,6 +106,10 @@ pub struct RsoHeader {
pub import_table_name_offset: u32,
}
impl RsoHeader {
pub fn new() -> Self { Self { version: 1, ..Default::default() } }
}
impl FromReader for RsoHeader {
type Args = ();
@ -205,13 +214,46 @@ impl FromReader for RsoHeader {
}
}
#[derive(Copy, Clone, Debug)]
impl ToWriter for RsoHeader {
fn to_writer<W>(&self, writer: &mut W, e: Endian) -> io::Result<()>
where W: Write + ?Sized {
(0u64).to_writer(writer, e)?; // next and prev
self.num_sections.to_writer(writer, e)?;
self.section_info_offset.to_writer(writer, e)?;
self.name_offset.to_writer(writer, e)?;
self.name_size.to_writer(writer, e)?;
self.version.to_writer(writer, e)?;
self.bss_size.to_writer(writer, e)?;
self.prolog_section.to_writer(writer, e)?;
self.epilog_section.to_writer(writer, e)?;
self.unresolved_section.to_writer(writer, e)?;
(0u8).to_writer(writer, e)?; // bss_section
self.prolog_offset.to_writer(writer, e)?;
self.epilog_offset.to_writer(writer, e)?;
self.unresolved_offset.to_writer(writer, e)?;
self.internal_rel_offset.to_writer(writer, e)?;
self.internal_rel_size.to_writer(writer, e)?;
self.external_rel_offset.to_writer(writer, e)?;
self.external_rel_size.to_writer(writer, e)?;
self.export_table_offset.to_writer(writer, e)?;
self.export_table_size.to_writer(writer, e)?;
self.export_table_name_offset.to_writer(writer, e)?;
self.import_table_offset.to_writer(writer, e)?;
self.import_table_size.to_writer(writer, e)?;
self.import_table_name_offset.to_writer(writer, e)?;
Ok(())
}
fn write_size(&self) -> usize { Self::STATIC_SIZE }
}
#[derive(Copy, Clone, Debug, Default)]
pub struct RsoSectionHeader {
/// Absolute offset of the section.
/// The lowest bit is set if the section is executable.
offset_and_flags: u32,
pub offset_and_flags: u32,
/// Size of the section.
size: u32,
pub size: u32,
}
impl FromReader for RsoSectionHeader {
@ -255,17 +297,17 @@ impl RsoSectionHeader {
pub fn exec(&self) -> bool { self.offset_and_flags & 1 != 0 }
}
struct RsoRelocation {
pub struct RsoRelocation {
/// Absolute offset of this relocation (relative to the start of the RSO file).
offset: u32,
pub offset: u32,
/// For internal relocations, this is the section index of the symbol being patched to.
/// For external relocations, this is the index of the symbol within the import symbol table.
/// The lowest 8 bits are the relocation type.
id_and_type: u32,
pub id_and_type: u32,
/// For internal relocations, this is the section-relative offset of the target symbol.
/// For external relocations, this is unused and always 0 (the offset is calculated using the
/// import symbol table).
target_offset: u32,
pub target_offset: u32,
}
impl FromReader for RsoRelocation {
@ -315,22 +357,23 @@ impl RsoRelocation {
}
#[derive(Copy, Clone, Debug, PartialEq, Eq)]
enum RsoSymbolKind {
pub enum RsoSymbolKind {
Import,
Export,
}
struct RsoSymbol {
#[derive(Debug)]
pub struct RsoSymbol {
/// Relative offset into the name table pointed to in the header,
/// which points to the name of this symbol.
name_offset: u32,
pub name_offset: u32,
/// The section-relative offset to the symbol. This is always 0 for imports.
offset: u32,
pub offset: u32,
/// For exports, index of the section that contains this symbol.
/// For imports, appears to be an offset?
section_index: u32,
/// For imports, offset of the first relocation that use this symbol
pub section_index: u32,
/// A hash of the symbol name. Only present for exports.
hash: Option<u32>,
pub hash: Option<u32>,
}
impl FromReader for RsoSymbol {
@ -360,7 +403,8 @@ impl ToWriter for RsoSymbol {
self.offset.to_writer(writer, e)?;
self.section_index.to_writer(writer, e)?;
if let Some(hash) = self.hash {
hash.to_writer(writer, e)?;
// Since the nature of the value is not numeric, we must preserve the order of the bytes
writer.write_all(&hash.to_ne_bytes())?;
}
Ok(())
}
@ -408,7 +452,7 @@ where R: Read + Seek + ?Sized {
size: size as u64,
data,
align: 0,
elf_index: idx as usize,
elf_index: idx as SectionIndex,
relocations: Default::default(),
virtual_address: None, // TODO option to set?
file_offset: offset as u64,
@ -432,13 +476,13 @@ where R: Read + Seek + ?Sized {
let (section_index, _) = sections
.iter()
.enumerate()
.find(|&(_, section)| section.elf_index == rel_section_idx as usize)
.find(|&(_, section)| section.elf_index == rel_section_idx as SectionIndex)
.ok_or_else(|| anyhow!("Failed to locate {name} section {rel_section_idx}"))?;
log::debug!("Adding {name} section {rel_section_idx} offset {offset:#X}");
symbols.push(ObjSymbol {
name: name.to_string(),
address: offset as u64,
section: Some(section_index),
section: Some(section_index as SectionIndex),
flags: ObjSymbolFlagSet(ObjSymbolFlags::Global.into()),
kind: ObjSymbolKind::Function,
..Default::default()
@ -482,7 +526,7 @@ where R: Read + Seek + ?Sized {
let section = sections
.iter()
.enumerate()
.find(|&(_, section)| section.elf_index == symbol.section_index as usize)
.find(|&(_, section)| section.elf_index == symbol.section_index as SectionIndex)
.map(|(idx, _)| idx)
// HACK: selfiles won't have any sections
.unwrap_or(symbol.section_index as usize);
@ -497,7 +541,7 @@ where R: Read + Seek + ?Sized {
name,
demangled_name,
address: symbol.offset as u64,
section: Some(section),
section: Some(section as SectionIndex),
..Default::default()
});
}
@ -524,7 +568,7 @@ where R: Read + Seek + ?Sized {
Ok(obj)
}
fn symbol_hash(s: &str) -> u32 {
pub fn symbol_hash(s: &str) -> u32 {
s.bytes().fold(0u32, |hash, c| {
let mut m = (hash << 4).wrapping_add(c as u32);
let n = m & 0xF0000000;

View File

@ -1,13 +1,11 @@
use std::{
collections::{btree_map, BTreeMap},
path::Path,
};
use std::collections::{btree_map, BTreeMap};
use anyhow::{anyhow, bail, ensure, Result};
use base64::{engine::general_purpose::STANDARD, Engine};
use cwdemangle::{demangle, DemangleOptions};
use serde::{Deserialize, Serialize};
use sha1::{Digest, Sha1};
use typed_path::Utf8NativePath;
use crate::{
analysis::{
@ -18,7 +16,7 @@ use crate::{
array_ref,
obj::{
ObjInfo, ObjKind, ObjReloc, ObjRelocKind, ObjSection, ObjSymbol, ObjSymbolFlagSet,
ObjSymbolKind,
ObjSymbolKind, SectionIndex, SymbolIndex,
},
util::elf::process_elf,
};
@ -36,13 +34,13 @@ pub struct OutSymbol {
pub struct OutReloc {
pub offset: u32,
pub kind: ObjRelocKind,
pub symbol: usize,
pub symbol: u32,
pub addend: i32,
}
#[derive(Debug, Clone, Hash, Eq, PartialEq, Serialize, Deserialize)]
pub struct FunctionSignature {
pub symbol: usize,
pub symbol: u32,
pub hash: String,
pub signature: String,
pub symbols: Vec<OutSymbol>,
@ -92,12 +90,12 @@ pub fn check_signatures(
let mut name = None;
for signature in signatures {
if name.is_none() {
name = Some(signature.symbols[signature.symbol].name.clone());
name = Some(signature.symbols[signature.symbol as usize].name.clone());
}
if check_signature(data, signature)? {
log::debug!(
"Found {} @ {:#010X} (hash {})",
signature.symbols[signature.symbol].name,
signature.symbols[signature.symbol as usize].name,
addr,
signature.hash
);
@ -114,9 +112,9 @@ pub fn apply_symbol(
obj: &mut ObjInfo,
target: SectionAddress,
sig_symbol: &OutSymbol,
) -> Result<usize> {
) -> Result<SymbolIndex> {
let mut target_section_index =
if target.section == usize::MAX { None } else { Some(target.section) };
if target.section == SectionIndex::MAX { None } else { Some(target.section) };
if let Some(target_section_index) = target_section_index {
let target_section = &mut obj.sections[target_section_index];
if !target_section.section_known {
@ -154,7 +152,7 @@ pub fn apply_signature(
addr: SectionAddress,
signature: &FunctionSignature,
) -> Result<()> {
let in_symbol = &signature.symbols[signature.symbol];
let in_symbol = &signature.symbols[signature.symbol as usize];
let symbol_idx = apply_symbol(obj, addr, in_symbol)?;
let mut tracker = Tracker::new(obj);
for reloc in &signature.relocations {
@ -185,7 +183,7 @@ pub fn apply_signature(
}
_ => bail!("Relocation mismatch: {:?} != {:?}", reloc, sig_reloc.kind),
};
let sig_symbol = &signature.symbols[sig_reloc.symbol];
let sig_symbol = &signature.symbols[sig_reloc.symbol as usize];
// log::info!("Processing relocation {:#010X} {:?} -> {:#010X} {:?}", reloc_addr, reloc, target, sig_symbol);
let target_symbol_idx = apply_symbol(obj, target, sig_symbol)?;
let obj_reloc = ObjReloc {
@ -200,7 +198,7 @@ pub fn apply_signature(
for reloc in &signature.relocations {
let addr = addr + reloc.offset;
if !tracker.relocations.contains_key(&addr) {
let sig_symbol = &signature.symbols[reloc.symbol];
let sig_symbol = &signature.symbols[reloc.symbol as usize];
bail!("Missing relocation @ {:#010X}: {:?} -> {:?}", addr, reloc, sig_symbol);
}
}
@ -246,11 +244,13 @@ pub fn compare_signature(existing: &mut FunctionSignature, new: &FunctionSignatu
Ok(())
}
pub fn generate_signature<P>(path: P, symbol_name: &str) -> Result<Option<FunctionSignature>>
where P: AsRef<Path> {
pub fn generate_signature(
path: &Utf8NativePath,
symbol_name: &str,
) -> Result<Option<FunctionSignature>> {
let mut out_symbols: Vec<OutSymbol> = Vec::new();
let mut out_relocs: Vec<OutReloc> = Vec::new();
let mut symbol_map: BTreeMap<usize, usize> = BTreeMap::new();
let mut symbol_map: BTreeMap<SymbolIndex, u32> = BTreeMap::new();
let mut obj = process_elf(path)?;
if obj.kind == ObjKind::Executable
@ -312,7 +312,7 @@ where P: AsRef<Path> {
let symbol_idx = match symbol_map.entry(reloc.target_symbol) {
btree_map::Entry::Vacant(e) => {
let target = &obj.symbols[reloc.target_symbol];
let symbol_idx = out_symbols.len();
let symbol_idx = out_symbols.len() as u32;
e.insert(symbol_idx);
out_symbols.push(OutSymbol {
kind: target.kind,

View File

@ -1,6 +1,6 @@
use std::{
cmp::{max, min, Ordering},
collections::{BTreeMap, HashMap, HashSet},
collections::{btree_map, BTreeMap, HashMap, HashSet},
};
use anyhow::{anyhow, bail, ensure, Context, Result};
@ -15,7 +15,7 @@ use crate::{
obj::{
ObjArchitecture, ObjInfo, ObjKind, ObjReloc, ObjRelocations, ObjSection, ObjSectionKind,
ObjSplit, ObjSymbol, ObjSymbolFlagSet, ObjSymbolFlags, ObjSymbolKind, ObjSymbolScope,
ObjUnit,
ObjUnit, SectionIndex, SymbolIndex,
},
util::{align_up, comment::MWComment},
};
@ -444,8 +444,8 @@ fn create_gap_splits(obj: &mut ObjInfo) -> Result<()> {
/// Ensures that all .bss splits following a common split are also marked as common.
fn update_common_splits(obj: &mut ObjInfo, common_start: Option<u32>) -> Result<()> {
let Some((bss_section_index, common_bss_start)) = (match common_start {
Some(addr) => Some((
let Some(common_bss_start) = (match common_start {
Some(addr) => Some(SectionAddress::new(
obj.sections.by_name(".bss")?.ok_or_else(|| anyhow!("Failed to find .bss section"))?.0,
addr,
)),
@ -454,8 +454,8 @@ fn update_common_splits(obj: &mut ObjInfo, common_start: Option<u32>) -> Result<
return Ok(());
};
log::debug!("Found common BSS start at {:#010X}", common_bss_start);
let bss_section = &mut obj.sections[bss_section_index];
for (addr, split) in bss_section.splits.for_range_mut(common_bss_start..) {
let bss_section = &mut obj.sections[common_bss_start.section];
for (addr, split) in bss_section.splits.for_range_mut(common_bss_start.address..) {
if !split.common {
split.common = true;
log::debug!("Added common flag to split {} at {:#010X}", split.unit, addr);
@ -593,8 +593,8 @@ fn add_padding_symbols(obj: &mut ObjInfo) -> Result<()> {
.peekable();
while let Some((_, symbol)) = iter.next() {
// Common BSS is allowed to have gaps and overlaps to accurately match the common BSS inflation bug
if matches!(common_bss, Some((idx, addr)) if
section_index == idx && symbol.address as u32 >= addr)
if matches!(common_bss, Some(addr) if
section_index == addr.section && symbol.address as u32 >= addr.address)
{
continue;
}
@ -758,26 +758,32 @@ fn trim_linker_generated_symbols(obj: &mut ObjInfo) -> Result<()> {
pub fn update_splits(obj: &mut ObjInfo, common_start: Option<u32>, fill_gaps: bool) -> Result<()> {
// Create splits for extab and extabindex entries
if let Some((section_index, section)) = obj.sections.by_name("extabindex")? {
let start = SectionAddress::new(section_index, section.address as u32);
split_extabindex(obj, start)?;
if !section.data.is_empty() {
let start = SectionAddress::new(section_index, section.address as u32);
split_extabindex(obj, start)?;
}
}
// Create splits for .ctors entries
if let Some((section_index, section)) = obj.sections.by_name(".ctors")? {
let start = SectionAddress::new(section_index, section.address as u32);
let end = start + (section.size as u32 - 4);
split_ctors_dtors(obj, start, end)?;
if !section.data.is_empty() {
let start = SectionAddress::new(section_index, section.address as u32);
let end = start + (section.size as u32 - 4);
split_ctors_dtors(obj, start, end)?;
}
}
// Create splits for .dtors entries
if let Some((section_index, section)) = obj.sections.by_name(".dtors")? {
let mut start = SectionAddress::new(section_index, section.address as u32);
let end = start + (section.size as u32 - 4);
if obj.kind == ObjKind::Executable {
// Skip __destroy_global_chain_reference
start += 4;
if !section.data.is_empty() {
let mut start = SectionAddress::new(section_index, section.address as u32);
let end = start + (section.size as u32 - 4);
if obj.kind == ObjKind::Executable {
// Skip __destroy_global_chain_reference
start += 4;
}
split_ctors_dtors(obj, start, end)?;
}
split_ctors_dtors(obj, start, end)?;
}
// Remove linker generated symbols from splits
@ -816,8 +822,8 @@ fn resolve_link_order(obj: &ObjInfo) -> Result<Vec<ObjUnit>> {
#[allow(dead_code)]
#[derive(Debug, Copy, Clone)]
struct SplitEdge {
from: u32,
to: u32,
from: i64,
to: i64,
}
let mut graph = Graph::<String, SplitEdge>::new();
@ -852,11 +858,36 @@ fn resolve_link_order(obj: &ObjInfo) -> Result<Vec<ObjUnit>> {
);
let a_index = *unit_to_index_map.get(&a.unit).unwrap();
let b_index = *unit_to_index_map.get(&b.unit).unwrap();
graph.add_edge(a_index, b_index, SplitEdge { from: a_addr, to: b_addr });
graph.add_edge(a_index, b_index, SplitEdge {
from: a_addr as i64,
to: b_addr as i64,
});
}
}
}
// Apply link order constraints provided by the user
let mut ordered_units = BTreeMap::<i32, String>::new();
for unit in &obj.link_order {
if let Some(order) = unit.order {
match ordered_units.entry(order) {
btree_map::Entry::Vacant(entry) => {
entry.insert(unit.name.clone());
}
btree_map::Entry::Occupied(entry) => {
bail!("Duplicate order {} for units {} and {}", order, entry.get(), unit.name);
}
}
}
}
let mut iter = ordered_units
.into_iter()
.filter_map(|(order, unit)| unit_to_index_map.get(&unit).map(|&index| (order, index)))
.peekable();
while let (Some((a_order, a_index)), Some((b_order, b_index))) = (iter.next(), iter.peek()) {
graph.add_edge(a_index, *b_index, SplitEdge { from: a_order as i64, to: *b_order as i64 });
}
// use petgraph::{
// dot::{Config, Dot},
// graph::EdgeReference,
@ -886,6 +917,7 @@ fn resolve_link_order(obj: &ObjInfo) -> Result<Vec<ObjUnit>> {
name: name.clone(),
autogenerated: obj.is_unit_autogenerated(name),
comment_version: None,
order: None,
}
}
})
@ -901,11 +933,11 @@ fn resolve_link_order(obj: &ObjInfo) -> Result<Vec<ObjUnit>> {
#[instrument(level = "debug", skip(obj))]
pub fn split_obj(obj: &ObjInfo, module_name: Option<&str>) -> Result<Vec<ObjInfo>> {
let mut objects: Vec<ObjInfo> = vec![];
let mut object_symbols: Vec<Vec<Option<usize>>> = vec![];
let mut object_symbols: Vec<Vec<Option<SymbolIndex>>> = vec![];
let mut name_to_obj: HashMap<String, usize> = HashMap::new();
for unit in &obj.link_order {
name_to_obj.insert(unit.name.clone(), objects.len());
object_symbols.push(vec![None; obj.symbols.count()]);
object_symbols.push(vec![None; obj.symbols.count() as usize]);
let mut split_obj = ObjInfo::new(
ObjKind::Relocatable,
ObjArchitecture::PowerPc,
@ -1047,7 +1079,7 @@ pub fn split_obj(obj: &ObjInfo, module_name: Option<&str>) -> Result<Vec<ObjInfo
s.section == Some(section_index) && !is_linker_generated_label(&s.name)
})
{
if symbol_idxs[symbol_idx].is_some() {
if symbol_idxs[symbol_idx as usize].is_some() {
continue; // should never happen?
}
@ -1060,7 +1092,7 @@ pub fn split_obj(obj: &ObjInfo, module_name: Option<&str>) -> Result<Vec<ObjInfo
continue;
}
symbol_idxs[symbol_idx] = Some(split_obj.symbols.add_direct(ObjSymbol {
let new_index = split_obj.symbols.add_direct(ObjSymbol {
name: symbol.name.clone(),
demangled_name: symbol.demangled_name.clone(),
address: if split.common {
@ -1081,7 +1113,8 @@ pub fn split_obj(obj: &ObjInfo, module_name: Option<&str>) -> Result<Vec<ObjInfo
data_kind: symbol.data_kind,
name_hash: symbol.name_hash,
demangled_name_hash: symbol.demangled_name_hash,
})?);
})?;
symbol_idxs[symbol_idx as usize] = Some(new_index);
}
// For mwldeppc 2.7 and above, a .comment section is required to link without error
@ -1124,7 +1157,7 @@ pub fn split_obj(obj: &ObjInfo, module_name: Option<&str>) -> Result<Vec<ObjInfo
let symbol_idxs = &mut object_symbols[obj_idx];
for (_section_index, section) in out_obj.sections.iter_mut() {
for (reloc_address, reloc) in section.relocations.iter_mut() {
match symbol_idxs[reloc.target_symbol] {
match symbol_idxs[reloc.target_symbol as usize] {
Some(out_sym_idx) => {
reloc.target_symbol = out_sym_idx;
}
@ -1157,7 +1190,7 @@ pub fn split_obj(obj: &ObjInfo, module_name: Option<&str>) -> Result<Vec<ObjInfo
globalize_symbols.push((reloc.target_symbol, new_name));
}
symbol_idxs[reloc.target_symbol] = Some(out_sym_idx);
symbol_idxs[reloc.target_symbol as usize] = Some(out_sym_idx);
out_obj.symbols.add_direct(ObjSymbol {
name: target_sym.name.clone(),
demangled_name: target_sym.demangled_name.clone(),
@ -1201,7 +1234,7 @@ pub fn split_obj(obj: &ObjInfo, module_name: Option<&str>) -> Result<Vec<ObjInfo
// Upgrade local symbols to global if necessary
for (obj, symbol_map) in objects.iter_mut().zip(&object_symbols) {
for (globalize_idx, new_name) in &globalize_symbols {
if let Some(symbol_idx) = symbol_map[*globalize_idx] {
if let Some(symbol_idx) = symbol_map[*globalize_idx as usize] {
let mut symbol = obj.symbols[symbol_idx].clone();
symbol.name.clone_from(new_name);
if symbol.flags.is_local() {
@ -1216,7 +1249,7 @@ pub fn split_obj(obj: &ObjInfo, module_name: Option<&str>) -> Result<Vec<ObjInfo
// Extern linker generated symbols
for obj in &mut objects {
let mut replace_symbols = vec![];
for (symbol_idx, symbol) in obj.symbols.iter().enumerate() {
for (symbol_idx, symbol) in obj.symbols.iter() {
if is_linker_generated_label(&symbol.name) && symbol.section.is_some() {
log::debug!("Externing {:?} in {}", symbol, obj.name);
replace_symbols.push((symbol_idx, ObjSymbol {
@ -1320,12 +1353,15 @@ pub fn is_linker_generated_object(name: &str) -> bool {
}
/// Locate the end address of a section when excluding linker generated objects
pub fn end_for_section(obj: &ObjInfo, section_index: usize) -> Result<SectionAddress> {
pub fn end_for_section(obj: &ObjInfo, section_index: SectionIndex) -> Result<SectionAddress> {
let section = obj
.sections
.get(section_index)
.ok_or_else(|| anyhow!("Invalid section index: {}", section_index))?;
let mut section_end = (section.address + section.size) as u32;
if section.data.is_empty() {
return Ok(SectionAddress::new(section_index, section_end));
}
// .ctors and .dtors end with a linker-generated null pointer,
// adjust section size appropriately
if matches!(section.name.as_str(), ".ctors" | ".dtors")

View File

@ -1,14 +1,15 @@
use std::{borrow::Cow, ffi::CStr, mem::size_of};
use anyhow::Result;
use zerocopy::{big_endian::U32, AsBytes, FromBytes, FromZeroes};
use typed_path::Utf8UnixPath;
use zerocopy::{big_endian::U32, FromBytes, Immutable, IntoBytes, KnownLayout};
use crate::static_assert;
use crate::{static_assert, vfs::next_non_empty};
pub const U8_MAGIC: [u8; 4] = [0x55, 0xAA, 0x38, 0x2D];
/// U8 archive header.
#[derive(Clone, Debug, PartialEq, FromBytes, FromZeroes, AsBytes)]
#[derive(Clone, Debug, PartialEq, FromBytes, IntoBytes, Immutable, KnownLayout)]
#[repr(C, align(4))]
pub struct U8Header {
magic: [u8; 4],
@ -32,7 +33,7 @@ pub enum U8NodeKind {
}
/// An individual file system node.
#[derive(Clone, Debug, PartialEq, FromBytes, FromZeroes, AsBytes)]
#[derive(Copy, Clone, Debug, PartialEq, FromBytes, IntoBytes, Immutable, KnownLayout)]
#[repr(C, align(4))]
pub struct U8Node {
kind: u8,
@ -91,7 +92,7 @@ pub struct U8View<'a> {
impl<'a> U8View<'a> {
/// Create a new U8 view from a buffer.
pub fn new(buf: &'a [u8]) -> Result<Self, &'static str> {
let Some(header) = U8Header::ref_from_prefix(buf) else {
let Ok((header, _)) = U8Header::ref_from_prefix(buf) else {
return Err("Buffer not large enough for U8 header");
};
if header.magic != U8_MAGIC {
@ -101,7 +102,8 @@ impl<'a> U8View<'a> {
let nodes_buf = buf
.get(node_table_offset..node_table_offset + header.node_table_size.get() as usize)
.ok_or("U8 node table out of bounds")?;
let root_node = U8Node::ref_from_prefix(nodes_buf).ok_or("U8 root node not aligned")?;
let (root_node, _) =
U8Node::ref_from_prefix(nodes_buf).map_err(|_| "U8 root node not aligned")?;
if root_node.kind() != U8NodeKind::Directory {
return Err("U8 root node is not a directory");
}
@ -113,7 +115,8 @@ impl<'a> U8View<'a> {
return Err("U8 node table size mismatch");
}
let (nodes_buf, string_table) = nodes_buf.split_at(node_count * size_of::<U8Node>());
let nodes = U8Node::slice_from(nodes_buf).ok_or("U8 node table not aligned")?;
let nodes =
<[U8Node]>::ref_from_bytes(nodes_buf).map_err(|_| "U8 node table not aligned")?;
Ok(Self { header, nodes, string_table })
}
@ -121,7 +124,7 @@ impl<'a> U8View<'a> {
pub fn iter(&self) -> U8Iter { U8Iter { inner: self, idx: 1 } }
/// Get the name of a node.
pub fn get_name(&self, node: &U8Node) -> Result<Cow<str>, String> {
pub fn get_name(&self, node: U8Node) -> Result<Cow<str>, String> {
let name_buf = self.string_table.get(node.name_offset() as usize..).ok_or_else(|| {
format!(
"U8: name offset {} out of bounds (string table size: {})",
@ -136,22 +139,29 @@ impl<'a> U8View<'a> {
}
/// Finds a particular file or directory by path.
pub fn find(&self, path: &str) -> Option<(usize, &U8Node)> {
let mut split = path.trim_matches('/').split('/');
let mut current = split.next()?;
pub fn find(&self, path: &Utf8UnixPath) -> Option<(usize, U8Node)> {
let mut split = path.as_str().split('/');
let mut current = next_non_empty(&mut split);
if current.is_empty() {
return Some((0, self.nodes[0]));
}
let mut idx = 1;
let mut stop_at = None;
while let Some(node) = self.nodes.get(idx) {
if self.get_name(node).as_ref().map_or(false, |name| name.eq_ignore_ascii_case(current))
{
if let Some(next) = split.next() {
current = next;
} else {
while let Some(node) = self.nodes.get(idx).copied() {
if self.get_name(node).map_or(false, |name| name.eq_ignore_ascii_case(current)) {
current = next_non_empty(&mut split);
if current.is_empty() {
return Some((idx, node));
}
// Descend into directory
idx += 1;
stop_at = Some(node.length() as usize + idx);
if node.is_dir() {
// Descend into directory
idx += 1;
stop_at = Some(node.length() as usize + idx);
} else {
// Not a directory
break;
}
} else if node.is_dir() {
// Skip directory
idx = node.length() as usize;
@ -176,11 +186,11 @@ pub struct U8Iter<'a> {
}
impl<'a> Iterator for U8Iter<'a> {
type Item = (usize, &'a U8Node, Result<Cow<'a, str>, String>);
type Item = (usize, U8Node, Result<Cow<'a, str>, String>);
fn next(&mut self) -> Option<Self::Item> {
let idx = self.idx;
let node = self.inner.nodes.get(idx)?;
let node = self.inner.nodes.get(idx).copied()?;
let name = self.inner.get_name(node);
self.idx += 1;
Some((idx, node, name))

130
src/vfs/common.rs Normal file
View File

@ -0,0 +1,130 @@
use std::{
io,
io::{BufRead, Cursor, Read, Seek, SeekFrom},
sync::Arc,
};
use filetime::FileTime;
use super::{DiscStream, VfsFileType, VfsMetadata};
use crate::vfs::VfsFile;
#[derive(Clone)]
pub struct StaticFile {
inner: Cursor<Arc<[u8]>>,
mtime: Option<FileTime>,
}
impl StaticFile {
pub fn new(data: Arc<[u8]>, mtime: Option<FileTime>) -> Self {
Self { inner: Cursor::new(data), mtime }
}
}
impl BufRead for StaticFile {
fn fill_buf(&mut self) -> io::Result<&[u8]> { self.inner.fill_buf() }
fn consume(&mut self, amt: usize) { self.inner.consume(amt) }
}
impl Read for StaticFile {
fn read(&mut self, buf: &mut [u8]) -> io::Result<usize> { self.inner.read(buf) }
}
impl Seek for StaticFile {
fn seek(&mut self, pos: SeekFrom) -> io::Result<u64> { self.inner.seek(pos) }
}
impl VfsFile for StaticFile {
fn map(&mut self) -> io::Result<&[u8]> { Ok(self.inner.get_ref()) }
fn metadata(&mut self) -> io::Result<VfsMetadata> {
Ok(VfsMetadata {
file_type: VfsFileType::File,
len: self.inner.get_ref().len() as u64,
mtime: self.mtime,
})
}
fn into_disc_stream(self: Box<Self>) -> Box<dyn DiscStream> { self }
}
#[derive(Clone)]
pub struct WindowedFile {
base: Box<dyn VfsFile>,
pos: u64,
begin: u64,
end: u64,
}
impl WindowedFile {
pub fn new(mut base: Box<dyn VfsFile>, offset: u64, size: u64) -> io::Result<Self> {
base.seek(SeekFrom::Start(offset))?;
Ok(Self { base, pos: offset, begin: offset, end: offset + size })
}
#[inline]
pub fn len(&self) -> u64 { self.end - self.begin }
}
impl BufRead for WindowedFile {
fn fill_buf(&mut self) -> io::Result<&[u8]> {
let buf = self.base.fill_buf()?;
let remaining = self.end.saturating_sub(self.pos);
Ok(&buf[..buf.len().min(remaining as usize)])
}
fn consume(&mut self, amt: usize) {
let remaining = self.end.saturating_sub(self.pos);
let amt = amt.min(remaining as usize);
self.base.consume(amt);
self.pos += amt as u64;
}
}
impl Read for WindowedFile {
fn read(&mut self, buf: &mut [u8]) -> io::Result<usize> {
let remaining = self.end.saturating_sub(self.pos);
if remaining == 0 {
return Ok(0);
}
let len = buf.len().min(remaining as usize);
self.base.read(&mut buf[..len])
}
}
impl Seek for WindowedFile {
#[inline]
fn seek(&mut self, pos: SeekFrom) -> io::Result<u64> {
let mut pos = match pos {
SeekFrom::Start(p) => self.begin + p,
SeekFrom::End(p) => self.end.saturating_add_signed(p),
SeekFrom::Current(p) => self.pos.saturating_add_signed(p),
};
if pos < self.begin {
pos = self.begin;
} else if pos > self.end {
pos = self.end;
}
let result = self.base.seek(SeekFrom::Start(pos))?;
self.pos = result;
Ok(result - self.begin)
}
#[inline]
fn stream_position(&mut self) -> io::Result<u64> { Ok(self.pos) }
}
impl VfsFile for WindowedFile {
fn map(&mut self) -> io::Result<&[u8]> {
let buf = self.base.map()?;
Ok(&buf[self.pos as usize..self.end as usize])
}
fn metadata(&mut self) -> io::Result<VfsMetadata> {
let metadata = self.base.metadata()?;
Ok(VfsMetadata { file_type: VfsFileType::File, len: self.len(), mtime: metadata.mtime })
}
fn into_disc_stream(self: Box<Self>) -> Box<dyn DiscStream> { self }
}

337
src/vfs/disc.rs Normal file
View File

@ -0,0 +1,337 @@
use std::{
io,
io::{BufRead, Cursor, Read, Seek, SeekFrom},
sync::Arc,
};
use filetime::FileTime;
use nodtool::{
nod,
nod::{Disc, DiscStream, Fst, NodeKind, OwnedFileStream, PartitionBase, PartitionMeta},
};
use typed_path::Utf8UnixPath;
use zerocopy::IntoBytes;
use super::{
next_non_empty, StaticFile, Vfs, VfsError, VfsFile, VfsFileType, VfsMetadata, VfsResult,
};
#[derive(Clone)]
pub struct DiscFs {
disc: Arc<Disc>,
base: Box<dyn PartitionBase>,
meta: Box<PartitionMeta>,
mtime: Option<FileTime>,
}
enum SpecialDir {
Root,
Sys,
Disc,
}
enum DiscNode<'a> {
None,
Special(SpecialDir),
Node(Fst<'a>, usize, nod::Node),
Static(&'a [u8]),
}
impl DiscFs {
pub fn new(
disc: Arc<Disc>,
mut base: Box<dyn PartitionBase>,
mtime: Option<FileTime>,
) -> io::Result<Self> {
let meta = base.meta().map_err(nod_to_io_error)?;
Ok(Self { disc, base, meta, mtime })
}
fn find(&self, path: &Utf8UnixPath) -> VfsResult<DiscNode> {
let path = path.as_str().trim_matches('/');
let mut split = path.split('/');
let mut segment = next_non_empty(&mut split);
match segment.to_ascii_lowercase().as_str() {
"" => Ok(DiscNode::Special(SpecialDir::Root)),
"files" => {
let fst = Fst::new(&self.meta.raw_fst)?;
if next_non_empty(&mut split).is_empty() {
let root = fst.nodes[0];
return Ok(DiscNode::Node(fst, 0, root));
}
let remainder = &path[segment.len() + 1..];
match fst.find(remainder) {
Some((idx, node)) => Ok(DiscNode::Node(fst, idx, node)),
None => Ok(DiscNode::None),
}
}
"sys" => {
segment = next_non_empty(&mut split);
// No directories in sys
if !next_non_empty(&mut split).is_empty() {
return Ok(DiscNode::None);
}
match segment.to_ascii_lowercase().as_str() {
"" => Ok(DiscNode::Special(SpecialDir::Sys)),
"boot.bin" => Ok(DiscNode::Static(self.meta.raw_boot.as_slice())),
"bi2.bin" => Ok(DiscNode::Static(self.meta.raw_bi2.as_slice())),
"apploader.img" => Ok(DiscNode::Static(self.meta.raw_apploader.as_ref())),
"fst.bin" => Ok(DiscNode::Static(self.meta.raw_fst.as_ref())),
"main.dol" => Ok(DiscNode::Static(self.meta.raw_dol.as_ref())),
_ => Ok(DiscNode::None),
}
}
"disc" => {
if !self.disc.header().is_wii() {
return Ok(DiscNode::None);
}
segment = next_non_empty(&mut split);
// No directories in disc
if !next_non_empty(&mut split).is_empty() {
return Ok(DiscNode::None);
}
match segment.to_ascii_lowercase().as_str() {
"" => Ok(DiscNode::Special(SpecialDir::Disc)),
"header.bin" => Ok(DiscNode::Static(&self.disc.header().as_bytes()[..0x100])),
"region.bin" => {
Ok(DiscNode::Static(self.disc.region().ok_or(VfsError::NotFound)?))
}
_ => Ok(DiscNode::None),
}
}
"ticket.bin" => {
Ok(DiscNode::Static(self.meta.raw_ticket.as_deref().ok_or(VfsError::NotFound)?))
}
"tmd.bin" => {
Ok(DiscNode::Static(self.meta.raw_tmd.as_deref().ok_or(VfsError::NotFound)?))
}
"cert.bin" => {
Ok(DiscNode::Static(self.meta.raw_cert_chain.as_deref().ok_or(VfsError::NotFound)?))
}
"h3.bin" => {
Ok(DiscNode::Static(self.meta.raw_h3_table.as_deref().ok_or(VfsError::NotFound)?))
}
_ => Ok(DiscNode::None),
}
}
}
impl Vfs for DiscFs {
fn open(&mut self, path: &Utf8UnixPath) -> VfsResult<Box<dyn VfsFile>> {
match self.find(path)? {
DiscNode::None => Err(VfsError::NotFound),
DiscNode::Special(_) => Err(VfsError::IsADirectory),
DiscNode::Node(_, _, node) => match node.kind() {
NodeKind::File => {
if node.length() > 2048 {
let file = self.base.clone().into_open_file(node)?;
Ok(Box::new(DiscFile::new(file, self.mtime)))
} else {
let len = node.length() as usize;
let mut file = self.base.open_file(node)?;
let mut data = vec![0u8; len];
file.read_exact(&mut data)?;
Ok(Box::new(StaticFile::new(Arc::from(data.as_slice()), self.mtime)))
}
}
NodeKind::Directory => Err(VfsError::IsADirectory),
NodeKind::Invalid => Err(VfsError::from("FST: Invalid node kind")),
},
DiscNode::Static(data) => Ok(Box::new(StaticFile::new(Arc::from(data), self.mtime))),
}
}
fn exists(&mut self, path: &Utf8UnixPath) -> VfsResult<bool> {
Ok(!matches!(self.find(path)?, DiscNode::None))
}
fn read_dir(&mut self, path: &Utf8UnixPath) -> VfsResult<Vec<String>> {
match self.find(path)? {
DiscNode::None => Err(VfsError::NotFound),
DiscNode::Special(SpecialDir::Root) => {
let mut entries = vec!["files".to_string(), "sys".to_string()];
if self.disc.header().is_wii() {
entries.push("disc".to_string());
}
if self.meta.raw_cert_chain.is_some() {
entries.push("cert.bin".to_string());
}
if self.meta.raw_h3_table.is_some() {
entries.push("h3.bin".to_string());
}
if self.meta.raw_ticket.is_some() {
entries.push("ticket.bin".to_string());
}
if self.meta.raw_tmd.is_some() {
entries.push("tmd.bin".to_string());
}
Ok(entries)
}
DiscNode::Special(SpecialDir::Sys) => Ok(vec![
"boot.bin".to_string(),
"bi2.bin".to_string(),
"apploader.img".to_string(),
"fst.bin".to_string(),
"main.dol".to_string(),
]),
DiscNode::Special(SpecialDir::Disc) => {
let mut entries = Vec::new();
if self.disc.header().is_wii() {
entries.push("header.bin".to_string());
if self.disc.region().is_some() {
entries.push("region.bin".to_string());
}
}
Ok(entries)
}
DiscNode::Node(fst, idx, node) => {
match node.kind() {
NodeKind::File => return Err(VfsError::NotADirectory),
NodeKind::Directory => {}
NodeKind::Invalid => return Err(VfsError::from("FST: Invalid node kind")),
}
let mut entries = Vec::new();
let mut idx = idx + 1;
let end = node.length() as usize;
while idx < end {
let child = fst
.nodes
.get(idx)
.copied()
.ok_or_else(|| VfsError::from("FST: Node index out of bounds"))?;
entries.push(fst.get_name(child)?.to_string());
if child.is_dir() {
idx = child.length() as usize;
} else {
idx += 1;
}
}
Ok(entries)
}
DiscNode::Static(_) => Err(VfsError::NotADirectory),
}
}
fn metadata(&mut self, path: &Utf8UnixPath) -> VfsResult<VfsMetadata> {
match self.find(path)? {
DiscNode::None => Err(VfsError::NotFound),
DiscNode::Special(_) => {
Ok(VfsMetadata { file_type: VfsFileType::Directory, len: 0, mtime: self.mtime })
}
DiscNode::Node(_, _, node) => {
let (file_type, len) = match node.kind() {
NodeKind::File => (VfsFileType::File, node.length()),
NodeKind::Directory => (VfsFileType::Directory, 0),
NodeKind::Invalid => return Err(VfsError::from("FST: Invalid node kind")),
};
Ok(VfsMetadata { file_type, len, mtime: self.mtime })
}
DiscNode::Static(data) => Ok(VfsMetadata {
file_type: VfsFileType::File,
len: data.len() as u64,
mtime: self.mtime,
}),
}
}
}
#[derive(Clone)]
enum DiscFileInner {
Stream(OwnedFileStream),
Mapped(Cursor<Arc<[u8]>>),
}
#[derive(Clone)]
struct DiscFile {
inner: DiscFileInner,
mtime: Option<FileTime>,
}
impl DiscFile {
pub fn new(file: OwnedFileStream, mtime: Option<FileTime>) -> Self {
Self { inner: DiscFileInner::Stream(file), mtime }
}
fn convert_to_mapped(&mut self) {
match &mut self.inner {
DiscFileInner::Stream(stream) => {
let pos = stream.stream_position().unwrap();
stream.seek(SeekFrom::Start(0)).unwrap();
let mut data = vec![0u8; stream.len() as usize];
stream.read_exact(&mut data).unwrap();
let mut cursor = Cursor::new(Arc::from(data.as_slice()));
cursor.set_position(pos);
self.inner = DiscFileInner::Mapped(cursor);
}
DiscFileInner::Mapped(_) => {}
};
}
}
impl BufRead for DiscFile {
fn fill_buf(&mut self) -> io::Result<&[u8]> {
match &mut self.inner {
DiscFileInner::Stream(stream) => stream.fill_buf(),
DiscFileInner::Mapped(data) => data.fill_buf(),
}
}
fn consume(&mut self, amt: usize) {
match &mut self.inner {
DiscFileInner::Stream(stream) => stream.consume(amt),
DiscFileInner::Mapped(data) => data.consume(amt),
}
}
}
impl Read for DiscFile {
fn read(&mut self, buf: &mut [u8]) -> io::Result<usize> {
match &mut self.inner {
DiscFileInner::Stream(stream) => stream.read(buf),
DiscFileInner::Mapped(data) => data.read(buf),
}
}
}
impl Seek for DiscFile {
fn seek(&mut self, pos: SeekFrom) -> io::Result<u64> {
match &mut self.inner {
DiscFileInner::Stream(stream) => stream.seek(pos),
DiscFileInner::Mapped(data) => data.seek(pos),
}
}
}
impl VfsFile for DiscFile {
fn map(&mut self) -> io::Result<&[u8]> {
self.convert_to_mapped();
match &mut self.inner {
DiscFileInner::Stream(_) => unreachable!(),
DiscFileInner::Mapped(data) => Ok(data.get_ref()),
}
}
fn metadata(&mut self) -> io::Result<VfsMetadata> {
match &mut self.inner {
DiscFileInner::Stream(stream) => Ok(VfsMetadata {
file_type: VfsFileType::File,
len: stream.len(),
mtime: self.mtime,
}),
DiscFileInner::Mapped(data) => Ok(VfsMetadata {
file_type: VfsFileType::File,
len: data.get_ref().len() as u64,
mtime: self.mtime,
}),
}
}
fn into_disc_stream(self: Box<Self>) -> Box<dyn DiscStream> { self }
}
pub fn nod_to_io_error(e: nod::Error) -> io::Error {
match e {
nod::Error::Io(msg, e) => io::Error::new(e.kind(), format!("{}: {}", msg, e)),
e => io::Error::new(io::ErrorKind::InvalidData, e),
}
}

373
src/vfs/mod.rs Normal file
View File

@ -0,0 +1,373 @@
mod common;
mod disc;
mod rarc;
mod std_fs;
mod u8_arc;
use std::{
error::Error,
fmt::{Debug, Display, Formatter},
io,
io::{BufRead, Read, Seek, SeekFrom},
sync::Arc,
};
use anyhow::{anyhow, Context};
use common::{StaticFile, WindowedFile};
use disc::{nod_to_io_error, DiscFs};
use dyn_clone::DynClone;
use filetime::FileTime;
use nodtool::{nod, nod::DiscStream};
use rarc::RarcFs;
pub use std_fs::StdFs;
use typed_path::{Utf8NativePath, Utf8UnixPath, Utf8UnixPathBuf};
use u8_arc::U8Fs;
use crate::util::{
ncompress::{YAY0_MAGIC, YAZ0_MAGIC},
nlzss,
rarc::RARC_MAGIC,
u8_arc::U8_MAGIC,
};
pub trait Vfs: DynClone + Send + Sync {
fn open(&mut self, path: &Utf8UnixPath) -> VfsResult<Box<dyn VfsFile>>;
fn exists(&mut self, path: &Utf8UnixPath) -> VfsResult<bool>;
fn read_dir(&mut self, path: &Utf8UnixPath) -> VfsResult<Vec<String>>;
fn metadata(&mut self, path: &Utf8UnixPath) -> VfsResult<VfsMetadata>;
}
dyn_clone::clone_trait_object!(Vfs);
pub trait VfsFile: DiscStream + BufRead {
fn map(&mut self) -> io::Result<&[u8]>;
fn metadata(&mut self) -> io::Result<VfsMetadata>;
fn into_disc_stream(self: Box<Self>) -> Box<dyn DiscStream>;
}
#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)]
pub enum VfsFileType {
File,
Directory,
}
pub struct VfsMetadata {
pub file_type: VfsFileType,
pub len: u64,
pub mtime: Option<FileTime>,
}
impl VfsMetadata {
pub fn is_file(&self) -> bool { self.file_type == VfsFileType::File }
pub fn is_dir(&self) -> bool { self.file_type == VfsFileType::Directory }
}
dyn_clone::clone_trait_object!(VfsFile);
#[derive(Debug)]
pub enum VfsError {
NotFound,
NotADirectory,
IsADirectory,
IoError(io::Error),
Other(String),
}
pub type VfsResult<T, E = VfsError> = Result<T, E>;
impl From<io::Error> for VfsError {
fn from(e: io::Error) -> Self {
match e.kind() {
io::ErrorKind::NotFound => VfsError::NotFound,
// TODO: stabilized in Rust 1.83
// io::ErrorKind::NotADirectory => VfsError::NotADirectory,
// io::ErrorKind::IsADirectory => VfsError::IsADirectory,
_ => VfsError::IoError(e),
}
}
}
impl From<String> for VfsError {
fn from(e: String) -> Self { VfsError::Other(e) }
}
impl From<&str> for VfsError {
fn from(e: &str) -> Self { VfsError::Other(e.to_string()) }
}
impl Display for VfsError {
fn fmt(&self, f: &mut Formatter) -> std::fmt::Result {
match self {
VfsError::NotFound => write!(f, "File or directory not found"),
VfsError::IoError(e) => write!(f, "{}", e),
VfsError::Other(e) => write!(f, "{}", e),
VfsError::NotADirectory => write!(f, "Path is a file, not a directory"),
VfsError::IsADirectory => write!(f, "Path is a directory, not a file"),
}
}
}
impl Error for VfsError {}
#[derive(Debug, Copy, Clone, Eq, PartialEq)]
pub enum FileFormat {
Regular,
Compressed(CompressionKind),
Archive(ArchiveKind),
}
impl Display for FileFormat {
fn fmt(&self, f: &mut Formatter) -> std::fmt::Result {
match self {
FileFormat::Regular => write!(f, "File"),
FileFormat::Compressed(kind) => write!(f, "Compressed: {}", kind),
FileFormat::Archive(kind) => write!(f, "Archive: {}", kind),
}
}
}
#[derive(Debug, Copy, Clone, Eq, PartialEq)]
pub enum CompressionKind {
Yay0,
Yaz0,
Nlzss,
}
impl Display for CompressionKind {
fn fmt(&self, f: &mut Formatter) -> std::fmt::Result {
match self {
CompressionKind::Yay0 => write!(f, "Yay0"),
CompressionKind::Yaz0 => write!(f, "Yaz0"),
CompressionKind::Nlzss => write!(f, "NLZSS"),
}
}
}
#[derive(Debug, Copy, Clone, Eq, PartialEq)]
pub enum ArchiveKind {
Rarc,
U8,
Disc(nod::Format),
}
impl Display for ArchiveKind {
fn fmt(&self, f: &mut Formatter) -> std::fmt::Result {
match self {
ArchiveKind::Rarc => write!(f, "RARC"),
ArchiveKind::U8 => write!(f, "U8"),
ArchiveKind::Disc(format) => write!(f, "Disc ({})", format),
}
}
}
pub fn detect<R>(file: &mut R) -> io::Result<FileFormat>
where R: Read + Seek + ?Sized {
file.seek(SeekFrom::Start(0))?;
let mut magic = [0u8; 4];
match file.read_exact(&mut magic) {
Ok(_) => {}
Err(e) if e.kind() == io::ErrorKind::UnexpectedEof => return Ok(FileFormat::Regular),
Err(e) => return Err(e),
}
file.seek_relative(-4)?;
match magic {
YAY0_MAGIC => Ok(FileFormat::Compressed(CompressionKind::Yay0)),
YAZ0_MAGIC => Ok(FileFormat::Compressed(CompressionKind::Yaz0)),
RARC_MAGIC => Ok(FileFormat::Archive(ArchiveKind::Rarc)),
U8_MAGIC => Ok(FileFormat::Archive(ArchiveKind::U8)),
_ => {
let format = nod::Disc::detect(file)?;
file.seek(SeekFrom::Start(0))?;
match format {
Some(format) => Ok(FileFormat::Archive(ArchiveKind::Disc(format))),
None => Ok(FileFormat::Regular),
}
}
}
}
pub enum OpenResult {
File(Box<dyn VfsFile>, Utf8UnixPathBuf),
Directory(Box<dyn Vfs>, Utf8UnixPathBuf),
}
pub fn open_path(path: &Utf8NativePath, auto_decompress: bool) -> anyhow::Result<OpenResult> {
open_path_with_fs(Box::new(StdFs), path, auto_decompress)
}
pub fn open_path_with_fs(
mut fs: Box<dyn Vfs>,
path: &Utf8NativePath,
auto_decompress: bool,
) -> anyhow::Result<OpenResult> {
let path = path.with_unix_encoding();
let mut split = path.as_str().split(':').peekable();
let mut current_path = String::new();
let mut file: Option<Box<dyn VfsFile>> = None;
let mut segment = Utf8UnixPath::new("");
loop {
// Open the next segment if necessary
if file.is_none() {
segment = Utf8UnixPath::new(split.next().unwrap());
if !current_path.is_empty() {
current_path.push(':');
}
current_path.push_str(segment.as_str());
let file_type = match fs.metadata(segment) {
Ok(metadata) => metadata.file_type,
Err(VfsError::NotFound) => return Err(anyhow!("{} not found", current_path)),
Err(e) => return Err(e).context(format!("Failed to open {}", current_path)),
};
match file_type {
VfsFileType::File => {
file = Some(
fs.open(segment)
.with_context(|| format!("Failed to open {}", current_path))?,
);
}
VfsFileType::Directory => {
return if split.peek().is_some() {
Err(anyhow!("{} is not a file", current_path))
} else {
Ok(OpenResult::Directory(fs, segment.to_path_buf()))
}
}
}
}
let mut current_file = file.take().unwrap();
let format = detect(current_file.as_mut())
.with_context(|| format!("Failed to detect file type for {}", current_path))?;
if let Some(&next) = split.peek() {
match next {
"nlzss" => {
split.next();
file = Some(
decompress_file(current_file.as_mut(), CompressionKind::Nlzss)
.with_context(|| {
format!("Failed to decompress {} with NLZSS", current_path)
})?,
);
}
"yay0" => {
split.next();
file = Some(
decompress_file(current_file.as_mut(), CompressionKind::Yay0)
.with_context(|| {
format!("Failed to decompress {} with Yay0", current_path)
})?,
);
}
"yaz0" => {
split.next();
file = Some(
decompress_file(current_file.as_mut(), CompressionKind::Yaz0)
.with_context(|| {
format!("Failed to decompress {} with Yaz0", current_path)
})?,
);
}
_ => match format {
FileFormat::Regular => {
return Err(anyhow!("{} is not an archive", current_path))
}
FileFormat::Compressed(kind) => {
file =
Some(decompress_file(current_file.as_mut(), kind).with_context(
|| format!("Failed to decompress {}", current_path),
)?);
// Continue the loop to detect the new format
}
FileFormat::Archive(kind) => {
fs = open_fs(current_file, kind).with_context(|| {
format!("Failed to open container {}", current_path)
})?;
// Continue the loop to open the next segment
}
},
}
} else {
// No more segments, return as-is
return match format {
FileFormat::Compressed(kind) if auto_decompress => Ok(OpenResult::File(
decompress_file(current_file.as_mut(), kind)
.with_context(|| format!("Failed to decompress {}", current_path))?,
segment.to_path_buf(),
)),
_ => Ok(OpenResult::File(current_file, segment.to_path_buf())),
};
}
}
}
pub fn open_file(path: &Utf8NativePath, auto_decompress: bool) -> anyhow::Result<Box<dyn VfsFile>> {
open_file_with_fs(Box::new(StdFs), path, auto_decompress)
}
pub fn open_file_with_fs(
fs: Box<dyn Vfs>,
path: &Utf8NativePath,
auto_decompress: bool,
) -> anyhow::Result<Box<dyn VfsFile>> {
match open_path_with_fs(fs, path, auto_decompress)? {
OpenResult::File(file, _) => Ok(file),
OpenResult::Directory(_, _) => Err(VfsError::IsADirectory.into()),
}
}
pub fn open_fs(mut file: Box<dyn VfsFile>, kind: ArchiveKind) -> io::Result<Box<dyn Vfs>> {
let metadata = file.metadata()?;
match kind {
ArchiveKind::Rarc => Ok(Box::new(RarcFs::new(file)?)),
ArchiveKind::U8 => Ok(Box::new(U8Fs::new(file)?)),
ArchiveKind::Disc(_) => {
let disc =
Arc::new(nod::Disc::new_stream(file.into_disc_stream()).map_err(nod_to_io_error)?);
let partition =
disc.open_partition_kind(nod::PartitionKind::Data).map_err(nod_to_io_error)?;
Ok(Box::new(DiscFs::new(disc, partition, metadata.mtime)?))
}
}
}
pub fn decompress_file(
file: &mut dyn VfsFile,
kind: CompressionKind,
) -> io::Result<Box<dyn VfsFile>> {
let metadata = file.metadata()?;
match kind {
CompressionKind::Yay0 => {
let data = file.map()?;
let result = orthrus_ncompress::yay0::Yay0::decompress_from(data)
.map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e.to_string()))?;
Ok(Box::new(StaticFile::new(Arc::from(result), metadata.mtime)))
}
CompressionKind::Yaz0 => {
let data = file.map()?;
let result = orthrus_ncompress::yaz0::Yaz0::decompress_from(data)
.map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e.to_string()))?;
Ok(Box::new(StaticFile::new(Arc::from(result), metadata.mtime)))
}
CompressionKind::Nlzss => {
let result = nlzss::decompress(file)
.map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e.to_string()))?;
Ok(Box::new(StaticFile::new(Arc::from(result.as_slice()), metadata.mtime)))
}
}
}
#[inline]
pub fn next_non_empty<'a>(iter: &mut impl Iterator<Item = &'a str>) -> &'a str {
loop {
match iter.next() {
Some("") => continue,
Some(next) => break next,
None => break "",
}
}
}

78
src/vfs/rarc.rs Normal file
View File

@ -0,0 +1,78 @@
use std::io;
use typed_path::Utf8UnixPath;
use super::{Vfs, VfsError, VfsFile, VfsFileType, VfsMetadata, VfsResult, WindowedFile};
use crate::util::rarc::{RarcNodeKind, RarcView};
#[derive(Clone)]
pub struct RarcFs {
file: Box<dyn VfsFile>,
}
impl RarcFs {
pub fn new(file: Box<dyn VfsFile>) -> io::Result<Self> { Ok(Self { file }) }
fn view(&mut self) -> io::Result<RarcView> {
let data = self.file.map()?;
RarcView::new(data).map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e))
}
}
impl Vfs for RarcFs {
fn open(&mut self, path: &Utf8UnixPath) -> VfsResult<Box<dyn VfsFile>> {
let view = self.view()?;
match view.find(path) {
Some(RarcNodeKind::File(_, node)) => {
let offset = view.header.header_len() as u64
+ view.header.data_offset() as u64
+ node.data_offset() as u64;
let len = node.data_length() as u64;
let file = WindowedFile::new(self.file.clone(), offset, len)?;
Ok(Box::new(file))
}
Some(RarcNodeKind::Directory(_, _)) => Err(VfsError::IsADirectory),
None => Err(VfsError::NotFound),
}
}
fn exists(&mut self, path: &Utf8UnixPath) -> VfsResult<bool> {
let view = self.view()?;
Ok(view.find(path).is_some())
}
fn read_dir(&mut self, path: &Utf8UnixPath) -> VfsResult<Vec<String>> {
let view = self.view()?;
match view.find(path) {
Some(RarcNodeKind::Directory(_, dir)) => {
let mut entries = Vec::new();
for node in view.children(dir) {
let name = view.get_string(node.name_offset())?;
if name == "." || name == ".." {
continue;
}
entries.push(name.to_string());
}
Ok(entries)
}
Some(RarcNodeKind::File(_, _)) => Err(VfsError::NotADirectory),
None => Err(VfsError::NotFound),
}
}
fn metadata(&mut self, path: &Utf8UnixPath) -> VfsResult<VfsMetadata> {
let metadata = self.file.metadata()?;
let view = self.view()?;
match view.find(path) {
Some(RarcNodeKind::File(_, node)) => Ok(VfsMetadata {
file_type: VfsFileType::File,
len: node.data_length() as u64,
mtime: metadata.mtime,
}),
Some(RarcNodeKind::Directory(_, _)) => {
Ok(VfsMetadata { file_type: VfsFileType::Directory, len: 0, mtime: metadata.mtime })
}
None => Err(VfsError::NotFound),
}
}
}

108
src/vfs/std_fs.rs Normal file
View File

@ -0,0 +1,108 @@
use std::{
fs, io,
io::{BufRead, BufReader, Read, Seek, SeekFrom},
};
use filetime::FileTime;
use typed_path::{Utf8NativePathBuf, Utf8UnixPath};
use super::{DiscStream, Vfs, VfsFile, VfsFileType, VfsMetadata, VfsResult};
#[derive(Clone)]
pub struct StdFs;
impl Vfs for StdFs {
fn open(&mut self, path: &Utf8UnixPath) -> VfsResult<Box<dyn VfsFile>> {
let mut file = StdFile::new(path.with_encoding());
file.file()?; // Open the file now to check for errors
Ok(Box::new(file))
}
fn exists(&mut self, path: &Utf8UnixPath) -> VfsResult<bool> {
Ok(fs::exists(path.with_encoding())?)
}
fn read_dir(&mut self, path: &Utf8UnixPath) -> VfsResult<Vec<String>> {
let entries = fs::read_dir(path.with_encoding())?
.map(|entry| entry.map(|e| e.file_name().to_string_lossy().into_owned()))
.collect::<Result<Vec<_>, _>>()?;
Ok(entries)
}
fn metadata(&mut self, path: &Utf8UnixPath) -> VfsResult<VfsMetadata> {
let metadata = fs::metadata(path.with_encoding())?;
Ok(VfsMetadata {
file_type: if metadata.is_dir() { VfsFileType::Directory } else { VfsFileType::File },
len: metadata.len(),
mtime: Some(FileTime::from_last_modification_time(&metadata)),
})
}
}
pub struct StdFile {
path: Utf8NativePathBuf,
file: Option<BufReader<fs::File>>,
mmap: Option<memmap2::Mmap>,
}
impl Clone for StdFile {
#[inline]
fn clone(&self) -> Self { Self { path: self.path.clone(), file: None, mmap: None } }
}
impl StdFile {
#[inline]
pub fn new(path: Utf8NativePathBuf) -> Self { StdFile { path, file: None, mmap: None } }
pub fn file(&mut self) -> io::Result<&mut BufReader<fs::File>> {
if self.file.is_none() {
self.file = Some(BufReader::new(fs::File::open(&self.path)?));
}
Ok(self.file.as_mut().unwrap())
}
}
impl BufRead for StdFile {
#[inline]
fn fill_buf(&mut self) -> io::Result<&[u8]> { self.file()?.fill_buf() }
#[inline]
fn consume(&mut self, amt: usize) {
if let Ok(file) = self.file() {
file.consume(amt);
}
}
}
impl Read for StdFile {
#[inline]
fn read(&mut self, buf: &mut [u8]) -> io::Result<usize> { self.file()?.read(buf) }
}
impl Seek for StdFile {
#[inline]
fn seek(&mut self, pos: SeekFrom) -> io::Result<u64> { self.file()?.seek(pos) }
}
impl VfsFile for StdFile {
fn map(&mut self) -> io::Result<&[u8]> {
if self.file.is_none() {
self.file = Some(BufReader::new(fs::File::open(&self.path)?));
}
if self.mmap.is_none() {
self.mmap = Some(unsafe { memmap2::Mmap::map(self.file.as_ref().unwrap().get_ref())? });
}
Ok(self.mmap.as_ref().unwrap())
}
fn metadata(&mut self) -> io::Result<VfsMetadata> {
let metadata = fs::metadata(&self.path)?;
Ok(VfsMetadata {
file_type: if metadata.is_dir() { VfsFileType::Directory } else { VfsFileType::File },
len: metadata.len(),
mtime: Some(FileTime::from_last_modification_time(&metadata)),
})
}
fn into_disc_stream(self: Box<Self>) -> Box<dyn DiscStream> { self }
}

91
src/vfs/u8_arc.rs Normal file
View File

@ -0,0 +1,91 @@
use std::io;
use typed_path::Utf8UnixPath;
use super::{Vfs, VfsError, VfsFile, VfsFileType, VfsMetadata, VfsResult, WindowedFile};
use crate::util::u8_arc::{U8NodeKind, U8View};
#[derive(Clone)]
pub struct U8Fs {
file: Box<dyn VfsFile>,
}
impl U8Fs {
pub fn new(file: Box<dyn VfsFile>) -> io::Result<Self> { Ok(Self { file }) }
fn view(&mut self) -> io::Result<U8View> {
let data = self.file.map()?;
U8View::new(data).map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e))
}
}
impl Vfs for U8Fs {
fn open(&mut self, path: &Utf8UnixPath) -> VfsResult<Box<dyn VfsFile>> {
let view = self.view()?;
match view.find(path) {
Some((_, node)) => match node.kind() {
U8NodeKind::File => {
let offset = node.offset() as u64;
let len = node.length() as u64;
let file = WindowedFile::new(self.file.clone(), offset, len)?;
Ok(Box::new(file))
}
U8NodeKind::Directory => Err(VfsError::IsADirectory),
U8NodeKind::Invalid => Err(VfsError::from("U8: Invalid node kind")),
},
None => Err(VfsError::NotFound),
}
}
fn exists(&mut self, path: &Utf8UnixPath) -> VfsResult<bool> {
let view = self.view()?;
Ok(view.find(path).is_some())
}
fn read_dir(&mut self, path: &Utf8UnixPath) -> VfsResult<Vec<String>> {
let view = self.view()?;
match view.find(path) {
Some((idx, node)) => match node.kind() {
U8NodeKind::File => Err(VfsError::NotADirectory),
U8NodeKind::Directory => {
let mut entries = Vec::new();
let mut idx = idx + 1;
let end = node.length() as usize;
while idx < end {
let child = view.nodes.get(idx).copied().ok_or(VfsError::NotFound)?;
entries.push(view.get_name(child)?.to_string());
if child.is_dir() {
idx = child.length() as usize;
} else {
idx += 1;
}
}
Ok(entries)
}
U8NodeKind::Invalid => Err(VfsError::from("U8: Invalid node kind")),
},
None => Err(VfsError::NotFound),
}
}
fn metadata(&mut self, path: &Utf8UnixPath) -> VfsResult<VfsMetadata> {
let metdata = self.file.metadata()?;
let view = self.view()?;
match view.find(path) {
Some((_, node)) => match node.kind() {
U8NodeKind::File => Ok(VfsMetadata {
file_type: VfsFileType::File,
len: node.length() as u64,
mtime: metdata.mtime,
}),
U8NodeKind::Directory => Ok(VfsMetadata {
file_type: VfsFileType::Directory,
len: 0,
mtime: metdata.mtime,
}),
U8NodeKind::Invalid => Err(VfsError::from("U8: Invalid node kind")),
},
None => Err(VfsError::NotFound),
}
}
}