mirror of
https://github.com/encounter/decomp-toolkit.git
synced 2025-08-04 19:25:44 +00:00
Compare commits
No commits in common. "main" and "v1.2.0" have entirely different histories.
2
.github/workflows/build.yml
vendored
2
.github/workflows/build.yml
vendored
@ -61,7 +61,7 @@ jobs:
|
||||
continue-on-error: ${{ matrix.checks == 'advisories' }}
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: EmbarkStudios/cargo-deny-action@v2
|
||||
- uses: EmbarkStudios/cargo-deny-action@v1
|
||||
with:
|
||||
command: check ${{ matrix.checks }}
|
||||
|
||||
|
93
Cargo.lock
generated
93
Cargo.lock
generated
@ -249,7 +249,7 @@ dependencies = [
|
||||
"encode_unicode",
|
||||
"lazy_static",
|
||||
"libc",
|
||||
"unicode-width 0.1.14",
|
||||
"unicode-width",
|
||||
"windows-sys 0.52.0",
|
||||
]
|
||||
|
||||
@ -339,31 +339,28 @@ checksum = "c2e06f9bce634a3c898eb1e5cb949ff63133cbb218af93cc9b38b31d6f3ea285"
|
||||
|
||||
[[package]]
|
||||
name = "cwextab"
|
||||
version = "1.1.2"
|
||||
version = "1.0.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9dd95393b8cc20937e4757d9c22b89d016613e934c60dcb073bd8a5aade79fcf"
|
||||
checksum = "e5aa7f13cc2fcb2bcfd3abc51bdbbf8f1fb729a69ed8c05ecbaa1a42197d1842"
|
||||
dependencies = [
|
||||
"thiserror 2.0.12",
|
||||
"thiserror",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "decomp-toolkit"
|
||||
version = "1.7.0"
|
||||
version = "1.2.0"
|
||||
dependencies = [
|
||||
"aes",
|
||||
"anyhow",
|
||||
"ar",
|
||||
"argp",
|
||||
"base16ct",
|
||||
"base64",
|
||||
"byteorder",
|
||||
"cbc",
|
||||
"crossterm",
|
||||
"cwdemangle",
|
||||
"cwextab",
|
||||
"dyn-clone",
|
||||
"enable-ansi-support",
|
||||
"encoding_rs",
|
||||
"filetime",
|
||||
"fixedbitset 0.5.7",
|
||||
"flagset",
|
||||
@ -384,6 +381,7 @@ dependencies = [
|
||||
"once_cell",
|
||||
"orthrus-ncompress",
|
||||
"owo-colors",
|
||||
"petgraph",
|
||||
"ppc750cl",
|
||||
"rayon",
|
||||
"regex",
|
||||
@ -401,7 +399,6 @@ dependencies = [
|
||||
"tracing-attributes",
|
||||
"tracing-subscriber",
|
||||
"typed-path",
|
||||
"unicode-width 0.2.0",
|
||||
"xxhash-rust",
|
||||
"zerocopy",
|
||||
]
|
||||
@ -555,7 +552,7 @@ version = "0.2.21"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "14dbbfd5c71d70241ecf9e6f13737f7b5ce823821063188d7e46c41d371eebd5"
|
||||
dependencies = [
|
||||
"unicode-width 0.1.14",
|
||||
"unicode-width",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@ -572,9 +569,9 @@ checksum = "d2fabcfbdc87f4758337ca535fb41a6d701b65693ce38287d856d1674551ec9b"
|
||||
|
||||
[[package]]
|
||||
name = "hashbrown"
|
||||
version = "0.15.2"
|
||||
version = "0.15.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "bf151400ff0baff5465007dd2f3e717f3fe502074ca563069ce3a6629d07b289"
|
||||
checksum = "1e087f84d4f86bf4b218b927129862374b72199ae7d8657835f1e89000eea4fb"
|
||||
dependencies = [
|
||||
"foldhash",
|
||||
]
|
||||
@ -632,7 +629,7 @@ dependencies = [
|
||||
"instant",
|
||||
"number_prefix",
|
||||
"portable-atomic",
|
||||
"unicode-width 0.1.14",
|
||||
"unicode-width",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@ -873,7 +870,7 @@ dependencies = [
|
||||
"miniz_oxide",
|
||||
"rayon",
|
||||
"sha1",
|
||||
"thiserror 1.0.64",
|
||||
"thiserror",
|
||||
"zerocopy",
|
||||
"zstd",
|
||||
]
|
||||
@ -946,7 +943,7 @@ dependencies = [
|
||||
"proc-macro-crate",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.101",
|
||||
"syn 2.0.79",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@ -1110,7 +1107,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "479cf940fbbb3426c32c5d5176f62ad57549a0bb84773423ba8be9d089f5faba"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"syn 2.0.101",
|
||||
"syn 2.0.79",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@ -1124,9 +1121,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "proc-macro2"
|
||||
version = "1.0.95"
|
||||
version = "1.0.88"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "02b3e5e68a3a1a02aad3ec490a98007cbc13c37cbe84a3cd7b8e406d76e7f778"
|
||||
checksum = "7c3a7fc5db1e57d5a779a352c8cdb57b29aa4c40cc69c3a68a7fedc815fbf2f9"
|
||||
dependencies = [
|
||||
"unicode-ident",
|
||||
]
|
||||
@ -1158,7 +1155,7 @@ dependencies = [
|
||||
"prost",
|
||||
"prost-types",
|
||||
"regex",
|
||||
"syn 2.0.101",
|
||||
"syn 2.0.79",
|
||||
"tempfile",
|
||||
]
|
||||
|
||||
@ -1172,7 +1169,7 @@ dependencies = [
|
||||
"itertools",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.101",
|
||||
"syn 2.0.79",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@ -1374,7 +1371,7 @@ checksum = "243902eda00fad750862fc144cea25caca5e20d615af0a81bee94ca738f1df1f"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.101",
|
||||
"syn 2.0.79",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@ -1385,7 +1382,7 @@ checksum = "18d26a20a969b9e3fdf2fc2d9f21eda6c40e2de84c9408bb5d3b05d499aae711"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.101",
|
||||
"syn 2.0.79",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@ -1408,7 +1405,7 @@ checksum = "6c64451ba24fc7a6a2d60fc75dd9c83c90903b19028d4eff35e88fc1e86564e9"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.101",
|
||||
"syn 2.0.79",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@ -1527,7 +1524,7 @@ dependencies = [
|
||||
"heck",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.101",
|
||||
"syn 2.0.79",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@ -1549,7 +1546,7 @@ dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"rustversion",
|
||||
"syn 2.0.101",
|
||||
"syn 2.0.79",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@ -1584,9 +1581,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "syn"
|
||||
version = "2.0.101"
|
||||
version = "2.0.79"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8ce2b7fc941b3a24138a0a7cf8e858bfc6a992e7978a068a5c760deb0ed43caf"
|
||||
checksum = "89132cd0bf050864e1d38dc3bbc07a0eb8e7530af26344d3d2bbbef83499f590"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
@ -1609,7 +1606,7 @@ dependencies = [
|
||||
"serde",
|
||||
"serde_derive",
|
||||
"serde_json",
|
||||
"thiserror 1.0.64",
|
||||
"thiserror",
|
||||
"walkdir",
|
||||
]
|
||||
|
||||
@ -1632,16 +1629,7 @@ version = "1.0.64"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d50af8abc119fb8bb6dbabcfa89656f46f84aa0ac7688088608076ad2b459a84"
|
||||
dependencies = [
|
||||
"thiserror-impl 1.0.64",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "thiserror"
|
||||
version = "2.0.12"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "567b8a2dae586314f7be2a752ec7474332959c6460e02bde30d702a66d488708"
|
||||
dependencies = [
|
||||
"thiserror-impl 2.0.12",
|
||||
"thiserror-impl",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@ -1652,18 +1640,7 @@ checksum = "08904e7672f5eb876eaaf87e0ce17857500934f4981c4a0ab2b4aa98baac7fc3"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.101",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "thiserror-impl"
|
||||
version = "2.0.12"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7f7cf42b4507d8ea322120659672cf1b9dbb93f8f2d4ecfd6e51350ff5b17a1d"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.101",
|
||||
"syn 2.0.79",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@ -1712,7 +1689,7 @@ checksum = "34704c8d6ebcbc939824180af020566b01a7c01f80641264eba0999f6c2b6be7"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.101",
|
||||
"syn 2.0.79",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@ -1775,7 +1752,7 @@ dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"serde_derive_internals",
|
||||
"syn 2.0.101",
|
||||
"syn 2.0.79",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@ -1811,12 +1788,6 @@ version = "0.1.14"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7dd6e30e90baa6f72411720665d41d89b9a3d039dc45b8faea1ddd07f617f6af"
|
||||
|
||||
[[package]]
|
||||
name = "unicode-width"
|
||||
version = "0.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1fc81956842c57dac11422a97c3b8195a1ff727f06e85c84ed2e8aa277c9a0fd"
|
||||
|
||||
[[package]]
|
||||
name = "unsafe-libyaml"
|
||||
version = "0.2.11"
|
||||
@ -1873,7 +1844,7 @@ dependencies = [
|
||||
"once_cell",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.101",
|
||||
"syn 2.0.79",
|
||||
"wasm-bindgen-shared",
|
||||
]
|
||||
|
||||
@ -1895,7 +1866,7 @@ checksum = "26c6ab57572f7a24a4985830b120de1594465e5d500f24afe89e16b4e833ef68"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.101",
|
||||
"syn 2.0.79",
|
||||
"wasm-bindgen-backend",
|
||||
"wasm-bindgen-shared",
|
||||
]
|
||||
@ -2108,7 +2079,7 @@ checksum = "3ca22c4ad176b37bd81a565f66635bde3d654fe6832730c3e52e1018ae1655ee"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.101",
|
||||
"syn 2.0.79",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
@ -3,7 +3,7 @@ name = "decomp-toolkit"
|
||||
description = "Yet another GameCube/Wii decompilation toolkit."
|
||||
authors = ["Luke Street <luke@street.dev>"]
|
||||
license = "MIT OR Apache-2.0"
|
||||
version = "1.7.0"
|
||||
version = "1.2.0"
|
||||
edition = "2021"
|
||||
publish = false
|
||||
repository = "https://github.com/encounter/decomp-toolkit"
|
||||
@ -25,8 +25,6 @@ strip = "debuginfo"
|
||||
codegen-units = 1
|
||||
|
||||
[dependencies]
|
||||
encoding_rs = "0.8"
|
||||
aes = "0.8"
|
||||
anyhow = { version = "1.0", features = ["backtrace"] }
|
||||
ar = { git = "https://github.com/bjorn3/rust-ar.git", branch = "write_symbol_table" }
|
||||
argp = "0.3"
|
||||
@ -34,10 +32,9 @@ base16ct = "0.2"
|
||||
base64 = "0.22"
|
||||
byteorder = "1.5"
|
||||
typed-path = "0.9"
|
||||
cbc = "0.1"
|
||||
crossterm = "0.28"
|
||||
cwdemangle = "1.0"
|
||||
cwextab = "1.1"
|
||||
cwextab = "1.0"
|
||||
dyn-clone = "1.0"
|
||||
enable-ansi-support = "0.2"
|
||||
filetime = "0.2"
|
||||
@ -61,6 +58,7 @@ object = { version = "0.36", features = ["read_core", "std", "elf", "write_std"]
|
||||
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"
|
||||
@ -77,7 +75,6 @@ syntect = { version = "5.2", features = ["parsing", "regex-fancy", "dump-load"],
|
||||
tracing = "0.1"
|
||||
tracing-attributes = "0.1"
|
||||
tracing-subscriber = { version = "0.3", features = ["env-filter"] }
|
||||
unicode-width = "0.2"
|
||||
xxhash-rust = { version = "0.8", features = ["xxh3"] }
|
||||
zerocopy = { version = "0.8", features = ["derive"] }
|
||||
|
||||
|
34
README.md
34
README.md
@ -52,9 +52,6 @@ project structure and build system that uses decomp-toolkit under the hood.
|
||||
- [yay0 compress](#yay0-compress)
|
||||
- [yaz0 decompress](#yaz0-decompress)
|
||||
- [yaz0 compress](#yaz0-compress)
|
||||
- [wad info](#wad-info)
|
||||
- [wad extract](#wad-extract)
|
||||
- [wad verify](#wad-verify)
|
||||
|
||||
## Goals
|
||||
|
||||
@ -297,8 +294,6 @@ Dumps DWARF 1.1 information from an ELF file. (Does **not** support DWARF 2+)
|
||||
|
||||
```shell
|
||||
$ dtk dwarf dump input.elf
|
||||
# or, to include data that was stripped by MWLD
|
||||
$ dtk dwarf dump input.elf --include-erased
|
||||
```
|
||||
|
||||
### elf disasm
|
||||
@ -479,7 +474,6 @@ Supported containers:
|
||||
- Disc images (see [disc info](#disc-info) for supported formats)
|
||||
- RARC archives (older .arc)
|
||||
- U8 archives (newer .arc)
|
||||
- WAD files (Wii VC)
|
||||
|
||||
Supported compression formats are handled transparently:
|
||||
- Yay0 (SZP) / Yaz0 (SZS)
|
||||
@ -568,31 +562,3 @@ $ dtk yaz0 compress input.bin -o output.bin.yaz0
|
||||
# or, for batch processing
|
||||
$ dtk yaz0 compress rels/* -o rels
|
||||
```
|
||||
|
||||
### wad info
|
||||
|
||||
Prints information about a WAD file.
|
||||
|
||||
```shell
|
||||
$ dtk wad info input.wad
|
||||
```
|
||||
|
||||
### wad extract
|
||||
|
||||
> [!NOTE]
|
||||
> [vfs cp](#vfs-cp) is more flexible and supports WAD files.
|
||||
> This command is now equivalent to `dtk vfs cp input.wad: output_dir`
|
||||
|
||||
Extracts the contents of a WAD file.
|
||||
|
||||
```shell
|
||||
$ dtk wad extract input.wad -o output_dir
|
||||
```
|
||||
|
||||
### wad verify
|
||||
|
||||
Verifies the contents of a WAD file.
|
||||
|
||||
```shell
|
||||
$ dtk wad verify input.wad
|
||||
```
|
||||
|
@ -74,7 +74,6 @@ ignore = [
|
||||
#{ 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" },
|
||||
{ id = "RUSTSEC-2024-0384", reason = "unmaintained transient dependency, will be updated in next nod version" },
|
||||
]
|
||||
# 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.
|
||||
|
@ -1,6 +1,6 @@
|
||||
use std::{
|
||||
cmp::min,
|
||||
collections::{BTreeMap, BTreeSet},
|
||||
collections::BTreeMap,
|
||||
fmt::{Debug, Display, Formatter, UpperHex},
|
||||
ops::{Add, AddAssign, BitAnd, Sub},
|
||||
};
|
||||
@ -191,7 +191,7 @@ impl AnalyzerState {
|
||||
};
|
||||
obj.add_symbol(
|
||||
ObjSymbol {
|
||||
name: format!("jumptable_{address_str}"),
|
||||
name: format!("jumptable_{}", address_str),
|
||||
address: addr.address as u64,
|
||||
section: Some(addr.section),
|
||||
size: size as u64,
|
||||
@ -275,7 +275,7 @@ impl AnalyzerState {
|
||||
let (section_index, _) = obj
|
||||
.sections
|
||||
.at_address(entry)
|
||||
.context(format!("Entry point {entry:#010X} outside of any section"))?;
|
||||
.context(format!("Entry point {:#010X} outside of any section", entry))?;
|
||||
self.process_function_at(obj, SectionAddress::new(section_index, entry))?;
|
||||
}
|
||||
// Locate bounds for referenced functions until none are left
|
||||
@ -530,7 +530,7 @@ pub fn locate_sda_bases(obj: &mut ObjInfo) -> Result<bool> {
|
||||
let (section_index, _) = obj
|
||||
.sections
|
||||
.at_address(entry as u32)
|
||||
.context(format!("Entry point {entry:#010X} outside of any section"))?;
|
||||
.context(format!("Entry point {:#010X} outside of any section", entry))?;
|
||||
let entry_addr = SectionAddress::new(section_index, entry as u32);
|
||||
|
||||
let mut executor = Executor::new(obj);
|
||||
@ -572,26 +572,6 @@ pub fn locate_sda_bases(obj: &mut ObjInfo) -> Result<bool> {
|
||||
Some((sda2_base, sda_base)) => {
|
||||
obj.sda2_base = Some(sda2_base);
|
||||
obj.sda_base = Some(sda_base);
|
||||
obj.add_symbol(
|
||||
ObjSymbol {
|
||||
name: "_SDA2_BASE_".to_string(),
|
||||
address: sda2_base as u64,
|
||||
size_known: true,
|
||||
flags: ObjSymbolFlagSet(ObjSymbolFlags::Global.into()),
|
||||
..Default::default()
|
||||
},
|
||||
true,
|
||||
)?;
|
||||
obj.add_symbol(
|
||||
ObjSymbol {
|
||||
name: "_SDA_BASE_".to_string(),
|
||||
address: sda_base as u64,
|
||||
size_known: true,
|
||||
flags: ObjSymbolFlagSet(ObjSymbolFlags::Global.into()),
|
||||
..Default::default()
|
||||
},
|
||||
true,
|
||||
)?;
|
||||
Ok(true)
|
||||
}
|
||||
None => Ok(false),
|
||||
@ -601,7 +581,7 @@ pub fn locate_sda_bases(obj: &mut ObjInfo) -> Result<bool> {
|
||||
/// ProDG hardcodes .bss and .sbss section initialization in `entry`
|
||||
/// This function locates the memset calls and returns a list of
|
||||
/// (address, size) pairs for the .bss sections.
|
||||
pub fn locate_bss_memsets(obj: &ObjInfo) -> Result<Vec<(u32, u32)>> {
|
||||
pub fn locate_bss_memsets(obj: &mut ObjInfo) -> Result<Vec<(u32, u32)>> {
|
||||
let mut bss_sections: Vec<(u32, u32)> = Vec::new();
|
||||
let Some(entry) = obj.entry else {
|
||||
return Ok(bss_sections);
|
||||
@ -609,7 +589,7 @@ pub fn locate_bss_memsets(obj: &ObjInfo) -> Result<Vec<(u32, u32)>> {
|
||||
let (section_index, _) = obj
|
||||
.sections
|
||||
.at_address(entry as u32)
|
||||
.context(format!("Entry point {entry:#010X} outside of any section"))?;
|
||||
.context(format!("Entry point {:#010X} outside of any section", entry))?;
|
||||
let entry_addr = SectionAddress::new(section_index, entry as u32);
|
||||
|
||||
let mut executor = Executor::new(obj);
|
||||
@ -652,50 +632,3 @@ pub fn locate_bss_memsets(obj: &ObjInfo) -> Result<Vec<(u32, u32)>> {
|
||||
)?;
|
||||
Ok(bss_sections)
|
||||
}
|
||||
|
||||
/// Execute VM from specified entry point following inner-section branches and function calls,
|
||||
/// noting all branch targets outside the current section.
|
||||
pub fn locate_cross_section_branch_targets(
|
||||
obj: &ObjInfo,
|
||||
entry: SectionAddress,
|
||||
) -> Result<BTreeSet<SectionAddress>> {
|
||||
let mut branch_targets = BTreeSet::<SectionAddress>::new();
|
||||
let mut executor = Executor::new(obj);
|
||||
executor.push(entry, VM::new(), false);
|
||||
executor.run(
|
||||
obj,
|
||||
|ExecCbData { executor, vm, result, ins_addr, section: _, ins: _, block_start: _ }| {
|
||||
match result {
|
||||
StepResult::Continue | StepResult::LoadStore { .. } => {
|
||||
Ok(ExecCbResult::<()>::Continue)
|
||||
}
|
||||
StepResult::Illegal => bail!("Illegal instruction @ {}", ins_addr),
|
||||
StepResult::Jump(target) => {
|
||||
if let BranchTarget::Address(RelocationTarget::Address(addr)) = target {
|
||||
if addr.section == entry.section {
|
||||
executor.push(addr, vm.clone_all(), true);
|
||||
} else {
|
||||
branch_targets.insert(addr);
|
||||
}
|
||||
}
|
||||
Ok(ExecCbResult::EndBlock)
|
||||
}
|
||||
StepResult::Branch(branches) => {
|
||||
for branch in branches {
|
||||
if let BranchTarget::Address(RelocationTarget::Address(addr)) =
|
||||
branch.target
|
||||
{
|
||||
if addr.section == entry.section {
|
||||
executor.push(addr, branch.vm, true);
|
||||
} else {
|
||||
branch_targets.insert(addr);
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(ExecCbResult::Continue)
|
||||
}
|
||||
}
|
||||
},
|
||||
)?;
|
||||
Ok(branch_targets)
|
||||
}
|
||||
|
@ -183,7 +183,8 @@ fn get_jump_table_entries(
|
||||
let (section_index, _) =
|
||||
obj.sections.at_address(entry_addr).with_context(|| {
|
||||
format!(
|
||||
"Invalid jump table entry {entry_addr:#010X} at {cur_addr:#010X}"
|
||||
"Invalid jump table entry {:#010X} at {:#010X}",
|
||||
entry_addr, cur_addr
|
||||
)
|
||||
})?;
|
||||
entries.push(SectionAddress::new(section_index, entry_addr));
|
||||
@ -244,9 +245,7 @@ pub fn uniq_jump_table_entries(
|
||||
return Ok((BTreeSet::new(), 0));
|
||||
}
|
||||
let (entries, size) =
|
||||
get_jump_table_entries(obj, addr, size, from, function_start, function_end).with_context(
|
||||
|| format!("While fetching jump table entries starting at {addr:#010X}"),
|
||||
)?;
|
||||
get_jump_table_entries(obj, addr, size, from, function_start, function_end)?;
|
||||
Ok((BTreeSet::from_iter(entries.iter().cloned()), size))
|
||||
}
|
||||
|
||||
|
@ -2,7 +2,7 @@ use anyhow::Result;
|
||||
|
||||
use crate::{
|
||||
obj::{ObjDataKind, ObjInfo, ObjSectionKind, ObjSymbolKind, SymbolIndex},
|
||||
util::{config::is_auto_symbol, split::is_linker_generated_label},
|
||||
util::split::is_linker_generated_label,
|
||||
};
|
||||
|
||||
pub fn detect_objects(obj: &mut ObjInfo) -> Result<()> {
|
||||
@ -134,9 +134,7 @@ pub fn detect_strings(obj: &mut ObjInfo) -> Result<()> {
|
||||
StringResult::None => {}
|
||||
StringResult::String { length, terminated } => {
|
||||
let size = if terminated { length + 1 } else { length };
|
||||
if symbol.size == size as u64
|
||||
|| (is_auto_symbol(symbol) && symbol.size > size as u64)
|
||||
{
|
||||
if !symbol.size_known || symbol.size == size as u64 {
|
||||
let str = String::from_utf8_lossy(&data[..length]);
|
||||
log::debug!("Found string '{}' @ {}", str, symbol.name);
|
||||
symbols_set.push((symbol_idx, ObjDataKind::String, size));
|
||||
@ -144,9 +142,7 @@ pub fn detect_strings(obj: &mut ObjInfo) -> Result<()> {
|
||||
}
|
||||
StringResult::WString { length, str } => {
|
||||
let size = length + 2;
|
||||
if symbol.size == size as u64
|
||||
|| (is_auto_symbol(symbol) && symbol.size > size as u64)
|
||||
{
|
||||
if !symbol.size_known || symbol.size == size as u64 {
|
||||
log::debug!("Found wide string '{}' @ {}", str, symbol.name);
|
||||
symbols_set.push((symbol_idx, ObjDataKind::String16, size));
|
||||
}
|
||||
|
@ -31,8 +31,9 @@ impl AnalysisPass for FindTRKInterruptVectorTable {
|
||||
Ok(ret) => ret,
|
||||
Err(_) => continue,
|
||||
};
|
||||
let trk_table_bytes = TRK_TABLE_HEADER.as_bytes();
|
||||
if data.starts_with(trk_table_bytes) && data[trk_table_bytes.len()] == 0 {
|
||||
if data.starts_with(TRK_TABLE_HEADER.as_bytes())
|
||||
&& data[TRK_TABLE_HEADER.as_bytes().len()] == 0
|
||||
{
|
||||
log::debug!("Found gTRKInterruptVectorTable @ {:#010X}", start);
|
||||
state.known_symbols.entry(start).or_default().push(ObjSymbol {
|
||||
name: "gTRKInterruptVectorTable".to_string(),
|
||||
@ -101,7 +102,7 @@ impl AnalysisPass for FindSaveRestSleds {
|
||||
for i in reg_start..reg_end {
|
||||
let addr = start + (i - reg_start) * step_size;
|
||||
state.known_symbols.entry(addr).or_default().push(ObjSymbol {
|
||||
name: format!("{label}{i}"),
|
||||
name: format!("{}{}", label, i),
|
||||
address: addr.address as u64,
|
||||
section: Some(start.section),
|
||||
size_known: true,
|
||||
|
@ -45,17 +45,6 @@ type BlockRange = Range<SectionAddress>;
|
||||
|
||||
type InsCheck = dyn Fn(Ins) -> bool;
|
||||
|
||||
/// Stop searching for prologue/epilogue sequences if the next instruction
|
||||
/// is a branch or uses r0 or r1.
|
||||
fn is_end_of_seq(next: &Ins) -> bool {
|
||||
next.is_branch()
|
||||
|| next
|
||||
.defs()
|
||||
.iter()
|
||||
.chain(next.uses().iter())
|
||||
.any(|a| matches!(a, ppc750cl::Argument::GPR(ppc750cl::GPR(0 | 1))))
|
||||
}
|
||||
|
||||
#[inline(always)]
|
||||
fn check_sequence(
|
||||
section: &ObjSection,
|
||||
@ -63,26 +52,29 @@ fn check_sequence(
|
||||
ins: Option<Ins>,
|
||||
sequence: &[(&InsCheck, &InsCheck)],
|
||||
) -> Result<bool> {
|
||||
let ins = ins
|
||||
.or_else(|| disassemble(section, addr.address))
|
||||
.with_context(|| format!("Failed to disassemble instruction at {addr:#010X}"))?;
|
||||
let mut found = false;
|
||||
for &(first, second) in sequence {
|
||||
let Some(ins) = ins.or_else(|| disassemble(section, addr.address)) else {
|
||||
continue;
|
||||
};
|
||||
if !first(ins) {
|
||||
continue;
|
||||
}
|
||||
let mut current_addr = addr.address + 4;
|
||||
while let Some(next) = disassemble(section, current_addr) {
|
||||
if second(next) {
|
||||
return Ok(true);
|
||||
}
|
||||
if is_end_of_seq(&next) {
|
||||
// If we hit a branch or an instruction that uses r0 or r1, stop searching.
|
||||
break;
|
||||
}
|
||||
current_addr += 4;
|
||||
let Some(next) = disassemble(section, addr.address + 4) else {
|
||||
continue;
|
||||
};
|
||||
if second(next)
|
||||
// Also check the following instruction, in case the scheduler
|
||||
// put something in between.
|
||||
|| (!next.is_branch()
|
||||
&& matches!(disassemble(section, addr.address + 8), Some(ins) if second(ins)))
|
||||
{
|
||||
found = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
Ok(false)
|
||||
|
||||
Ok(found)
|
||||
}
|
||||
|
||||
fn check_prologue_sequence(
|
||||
@ -97,19 +89,15 @@ fn check_prologue_sequence(
|
||||
}
|
||||
#[inline(always)]
|
||||
fn is_stwu(ins: Ins) -> bool {
|
||||
// stwu[x] r1, d(r1)
|
||||
matches!(ins.op, Opcode::Stwu | Opcode::Stwux) && ins.field_rs() == 1 && ins.field_ra() == 1
|
||||
// stwu r1, d(r1)
|
||||
ins.op == Opcode::Stwu && ins.field_rs() == 1 && ins.field_ra() == 1
|
||||
}
|
||||
#[inline(always)]
|
||||
fn is_stw(ins: Ins) -> bool {
|
||||
// stw r0, d(r1)
|
||||
ins.op == Opcode::Stw && ins.field_rs() == 0 && ins.field_ra() == 1
|
||||
}
|
||||
check_sequence(section, addr, ins, &[
|
||||
(&is_stwu, &is_mflr),
|
||||
(&is_mflr, &is_stw),
|
||||
(&is_mflr, &is_stwu),
|
||||
])
|
||||
check_sequence(section, addr, ins, &[(&is_stwu, &is_mflr), (&is_mflr, &is_stw)])
|
||||
}
|
||||
|
||||
impl FunctionSlices {
|
||||
@ -160,29 +148,8 @@ impl FunctionSlices {
|
||||
}
|
||||
if check_prologue_sequence(section, addr, Some(ins))? {
|
||||
if let Some(prologue) = self.prologue {
|
||||
let invalid_seq = if prologue == addr {
|
||||
false
|
||||
} else if prologue > addr {
|
||||
true
|
||||
} else {
|
||||
// Check if any instructions between the prologue and this address
|
||||
// are branches or use r0 or r1.
|
||||
let mut current_addr = prologue.address + 4;
|
||||
loop {
|
||||
if current_addr == addr.address {
|
||||
break false;
|
||||
}
|
||||
let next = disassemble(section, current_addr).with_context(|| {
|
||||
format!("Failed to disassemble {current_addr:#010X}")
|
||||
})?;
|
||||
if is_end_of_seq(&next) {
|
||||
break true;
|
||||
}
|
||||
current_addr += 4;
|
||||
}
|
||||
};
|
||||
if invalid_seq {
|
||||
bail!("Found multiple functions inside a symbol: {:#010X} and {:#010X}. Check symbols.txt?", prologue, addr)
|
||||
if prologue != addr && prologue != addr - 4 {
|
||||
bail!("Found duplicate prologue: {:#010X} and {:#010X}", prologue, addr)
|
||||
}
|
||||
} else {
|
||||
self.prologue = Some(addr);
|
||||
@ -213,11 +180,7 @@ impl FunctionSlices {
|
||||
ins.op == Opcode::Or && ins.field_rd() == 1
|
||||
}
|
||||
|
||||
if check_sequence(section, addr, Some(ins), &[
|
||||
(&is_mtlr, &is_addi),
|
||||
(&is_mtlr, &is_or),
|
||||
(&is_or, &is_mtlr),
|
||||
])? {
|
||||
if check_sequence(section, addr, Some(ins), &[(&is_mtlr, &is_addi), (&is_or, &is_mtlr)])? {
|
||||
if let Some(epilogue) = self.epilogue {
|
||||
if epilogue != addr {
|
||||
bail!("Found duplicate epilogue: {:#010X} and {:#010X}", epilogue, addr)
|
||||
@ -264,7 +227,7 @@ impl FunctionSlices {
|
||||
})?;
|
||||
}
|
||||
self.check_epilogue(section, ins_addr, ins)
|
||||
.with_context(|| format!("While processing {function_start:#010X}: {self:#?}"))?;
|
||||
.with_context(|| format!("While processing {:#010X}: {:#?}", function_start, self))?;
|
||||
if !self.has_conditional_blr && is_conditional_blr(ins) {
|
||||
self.has_conditional_blr = true;
|
||||
}
|
||||
@ -377,14 +340,7 @@ impl FunctionSlices {
|
||||
function_end.or_else(|| self.end()),
|
||||
)?;
|
||||
log::debug!("-> size {}: {:?}", size, entries);
|
||||
let max_block = self
|
||||
.blocks
|
||||
.keys()
|
||||
.next_back()
|
||||
.copied()
|
||||
.unwrap_or(next_address)
|
||||
.max(next_address);
|
||||
if entries.iter().any(|&addr| addr > function_start && addr <= max_block)
|
||||
if (entries.contains(&next_address) || self.blocks.contains_key(&next_address))
|
||||
&& !entries.iter().any(|&addr| {
|
||||
self.is_known_function(known_functions, addr)
|
||||
.is_some_and(|fn_addr| fn_addr != function_start)
|
||||
@ -747,7 +703,7 @@ impl FunctionSlices {
|
||||
}
|
||||
}
|
||||
// If we discovered a function prologue, known tail call.
|
||||
if slices.prologue.is_some() || slices.has_r1_load {
|
||||
if slices.prologue.is_some() {
|
||||
log::trace!("Prologue discovered; known tail call: {:#010X}", addr);
|
||||
return TailCallResult::Is;
|
||||
}
|
||||
|
@ -269,8 +269,7 @@ impl Tracker {
|
||||
possible_missed_branches: &mut BTreeMap<SectionAddress, Box<VM>>,
|
||||
) -> Result<ExecCbResult<()>> {
|
||||
let ExecCbData { executor, vm, result, ins_addr, section: _, ins, block_start: _ } = data;
|
||||
// Using > instead of >= to treat a branch to the beginning of the function as a tail call
|
||||
let is_function_addr = |addr: SectionAddress| addr > function_start && addr < function_end;
|
||||
let is_function_addr = |addr: SectionAddress| addr >= function_start && addr < function_end;
|
||||
let _span = debug_span!("ins", addr = %ins_addr, op = ?ins.op).entered();
|
||||
|
||||
match result {
|
||||
@ -417,13 +416,9 @@ impl Tracker {
|
||||
Ok(ExecCbResult::Continue)
|
||||
}
|
||||
StepResult::Jump(target) => match target {
|
||||
BranchTarget::Return => Ok(ExecCbResult::EndBlock),
|
||||
BranchTarget::Unknown
|
||||
| BranchTarget::Return
|
||||
| BranchTarget::JumpTable { address: RelocationTarget::External, .. } => {
|
||||
let next_addr = ins_addr + 4;
|
||||
if next_addr < function_end {
|
||||
possible_missed_branches.insert(ins_addr + 4, vm.clone_all());
|
||||
}
|
||||
Ok(ExecCbResult::EndBlock)
|
||||
}
|
||||
BranchTarget::Address(addr) => {
|
||||
@ -580,7 +575,7 @@ impl Tracker {
|
||||
let relocation_target = relocation_target_for(obj, from, None).ok().flatten();
|
||||
if !matches!(relocation_target, None | Some(RelocationTarget::External)) {
|
||||
// VM should have already handled this
|
||||
panic!("Relocation already exists for {addr:#010X} (from {from:#010X})");
|
||||
panic!("Relocation already exists for {:#010X} (from {:#010X})", addr, from);
|
||||
}
|
||||
}
|
||||
// Remainder of this function is for executable objects only
|
||||
@ -672,7 +667,7 @@ impl Tracker {
|
||||
0
|
||||
};
|
||||
let new_name =
|
||||
if module_id == 0 { name.to_string() } else { format!("{name}:{module_id}") };
|
||||
if module_id == 0 { name.to_string() } else { format!("{}:{}", name, module_id) };
|
||||
log::debug!("Renaming {} to {}", section.name, new_name);
|
||||
section.name = new_name;
|
||||
}
|
||||
@ -738,65 +733,55 @@ impl Tracker {
|
||||
);
|
||||
}
|
||||
}
|
||||
let (data_kind, inferred_alignment) = self
|
||||
let data_kind = self
|
||||
.data_types
|
||||
.get(&target)
|
||||
.map(|dt| match dt {
|
||||
DataKind::Unknown => (ObjDataKind::Unknown, None),
|
||||
DataKind::Word => (ObjDataKind::Byte4, None),
|
||||
DataKind::Half => (ObjDataKind::Byte2, None),
|
||||
DataKind::Byte => (ObjDataKind::Byte, None),
|
||||
DataKind::Float => (ObjDataKind::Float, Some(4)),
|
||||
DataKind::Double => (ObjDataKind::Double, Some(8)),
|
||||
DataKind::Unknown => ObjDataKind::Unknown,
|
||||
DataKind::Word => ObjDataKind::Byte4,
|
||||
DataKind::Half => ObjDataKind::Byte2,
|
||||
DataKind::Byte => ObjDataKind::Byte,
|
||||
DataKind::Float => ObjDataKind::Float,
|
||||
DataKind::Double => ObjDataKind::Double,
|
||||
})
|
||||
.unwrap_or_default();
|
||||
let (target_symbol, addend) =
|
||||
if let Some(symbol) = self.special_symbol(obj, target.address, reloc_kind) {
|
||||
(symbol, 0)
|
||||
} else if let Some((symbol_idx, symbol)) =
|
||||
obj.symbols.for_relocation(target, reloc_kind)?
|
||||
let (target_symbol, addend) = if let Some(symbol) =
|
||||
self.special_symbol(obj, target.address, reloc_kind)
|
||||
{
|
||||
(symbol, 0)
|
||||
} else if let Some((symbol_idx, symbol)) =
|
||||
obj.symbols.for_relocation(target, reloc_kind)?
|
||||
{
|
||||
let symbol_address = symbol.address;
|
||||
// TODO meh
|
||||
if data_kind != ObjDataKind::Unknown
|
||||
&& symbol.data_kind == ObjDataKind::Unknown
|
||||
&& symbol_address as u32 == target.address
|
||||
{
|
||||
let symbol_address = symbol.address;
|
||||
if symbol_address as u32 == target.address
|
||||
&& ((data_kind != ObjDataKind::Unknown
|
||||
&& symbol.data_kind == ObjDataKind::Unknown)
|
||||
|| (symbol.align.is_none() && inferred_alignment.is_some()))
|
||||
{
|
||||
let mut new_symbol = symbol.clone();
|
||||
if symbol.data_kind == ObjDataKind::Unknown {
|
||||
new_symbol.data_kind = data_kind;
|
||||
}
|
||||
if symbol.align.is_none() {
|
||||
if let Some(inferred_alignment) = inferred_alignment {
|
||||
if symbol_address as u32 % inferred_alignment == 0 {
|
||||
new_symbol.align = Some(inferred_alignment);
|
||||
}
|
||||
}
|
||||
}
|
||||
obj.symbols.replace(symbol_idx, new_symbol)?;
|
||||
}
|
||||
(symbol_idx, target.address as i64 - symbol_address as i64)
|
||||
obj.symbols.replace(symbol_idx, ObjSymbol { data_kind, ..symbol.clone() })?;
|
||||
}
|
||||
(symbol_idx, target.address as i64 - symbol_address as i64)
|
||||
} else {
|
||||
// Create a new label
|
||||
let name = if obj.module_id == 0 {
|
||||
format!("lbl_{:08X}", target.address)
|
||||
} else {
|
||||
// Create a new label
|
||||
let name = if obj.module_id == 0 {
|
||||
format!("lbl_{:08X}", target.address)
|
||||
} else {
|
||||
format!(
|
||||
"lbl_{}_{}_{:X}",
|
||||
obj.module_id,
|
||||
obj.sections[target.section].name.trim_start_matches('.'),
|
||||
target.address
|
||||
)
|
||||
};
|
||||
let symbol_idx = obj.symbols.add_direct(ObjSymbol {
|
||||
name,
|
||||
address: target.address as u64,
|
||||
section: Some(target.section),
|
||||
data_kind,
|
||||
..Default::default()
|
||||
})?;
|
||||
(symbol_idx, 0)
|
||||
format!(
|
||||
"lbl_{}_{}_{:X}",
|
||||
obj.module_id,
|
||||
obj.sections[target.section].name.trim_start_matches('.'),
|
||||
target.address
|
||||
)
|
||||
};
|
||||
let symbol_idx = obj.symbols.add_direct(ObjSymbol {
|
||||
name,
|
||||
address: target.address as u64,
|
||||
section: Some(target.section),
|
||||
data_kind,
|
||||
..Default::default()
|
||||
})?;
|
||||
(symbol_idx, 0)
|
||||
};
|
||||
let reloc = ObjReloc { kind: reloc_kind, target_symbol, addend, module: None };
|
||||
let section = &mut obj.sections[addr.section];
|
||||
if replace {
|
||||
|
@ -127,16 +127,16 @@ fn extract(args: ExtractArgs) -> Result<()> {
|
||||
}
|
||||
std::fs::create_dir_all(&out_dir)?;
|
||||
if !args.quiet {
|
||||
println!("Extracting {path} to {out_dir}");
|
||||
println!("Extracting {} to {}", path, out_dir);
|
||||
}
|
||||
|
||||
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}"))?;
|
||||
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}");
|
||||
println!("\t{}", file_name);
|
||||
}
|
||||
let mut file_path = out_dir.clone();
|
||||
for segment in file_name.split(&['/', '\\']) {
|
||||
@ -146,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}"))?;
|
||||
.with_context(|| format!("Failed to create file {}", file_path))?;
|
||||
std::io::copy(&mut entry, &mut file)?;
|
||||
file.flush()?;
|
||||
|
||||
@ -154,7 +154,7 @@ fn extract(args: ExtractArgs) -> Result<()> {
|
||||
}
|
||||
}
|
||||
if !args.quiet {
|
||||
println!("Extracted {num_files} files");
|
||||
println!("Extracted {} files", num_files);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
239
src/cmd/dol.rs
239
src/cmd/dol.rs
@ -47,7 +47,6 @@ use crate::{
|
||||
diff::{calc_diff_ranges, print_diff, process_code},
|
||||
dol::process_dol,
|
||||
elf::{process_elf, write_elf},
|
||||
extab::clean_extab,
|
||||
file::{
|
||||
buf_copy_with_hash, buf_writer, check_hash_str, touch, verify_hash, FileIterator,
|
||||
FileReadInfo,
|
||||
@ -60,7 +59,7 @@ use crate::{
|
||||
split::{is_linker_generated_object, split_obj, update_splits},
|
||||
IntoCow, ToCow,
|
||||
},
|
||||
vfs::{detect, open_file, open_file_with_fs, open_fs, ArchiveKind, FileFormat, Vfs, VfsFile},
|
||||
vfs::{open_file, open_file_with_fs, open_fs, ArchiveKind, Vfs, VfsFile},
|
||||
};
|
||||
|
||||
#[derive(FromArgs, PartialEq, Debug)]
|
||||
@ -133,9 +132,6 @@ pub struct ApplyArgs {
|
||||
#[argp(positional, from_str_fn(native_path))]
|
||||
/// linked ELF
|
||||
elf_file: Utf8NativePathBuf,
|
||||
#[argp(switch)]
|
||||
/// always update anonymous local symbol names, even if they are similar
|
||||
full: bool,
|
||||
}
|
||||
|
||||
#[derive(FromArgs, PartialEq, Eq, Debug)]
|
||||
@ -294,17 +290,12 @@ pub struct ModuleConfig {
|
||||
pub block_relocations: Vec<BlockRelocationConfig>,
|
||||
#[serde(default, skip_serializing_if = "Vec::is_empty")]
|
||||
pub add_relocations: Vec<AddRelocationConfig>,
|
||||
/// Process exception tables and zero out uninitialized data.
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub clean_extab: Option<bool>,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)]
|
||||
pub struct ExtractConfig {
|
||||
/// The name of the symbol to extract.
|
||||
pub symbol: String,
|
||||
/// Optionally rename the output symbol. (e.g. symbol$1234 -> symbol)
|
||||
pub rename: Option<String>,
|
||||
/// If specified, the symbol's data will be extracted to the given file.
|
||||
/// Path is relative to `out_dir/bin`.
|
||||
#[serde(with = "unix_path_serde_option", default, skip_serializing_if = "Option::is_none")]
|
||||
@ -394,7 +385,6 @@ pub struct OutputModule {
|
||||
#[derive(Serialize, Deserialize, Debug, Clone, Default)]
|
||||
pub struct OutputExtract {
|
||||
pub symbol: String,
|
||||
pub rename: Option<String>,
|
||||
#[serde(with = "unix_path_serde_option")]
|
||||
pub binary: Option<Utf8UnixPathBuf>,
|
||||
#[serde(with = "unix_path_serde_option")]
|
||||
@ -538,11 +528,9 @@ pub fn info(args: InfoArgs) -> Result<()> {
|
||||
apply_selfile(&mut obj, file.map()?)?;
|
||||
}
|
||||
|
||||
if !obj.name.is_empty() {
|
||||
println!("{}:", obj.name);
|
||||
}
|
||||
println!("{}:", obj.name);
|
||||
if let Some(entry) = obj.entry {
|
||||
println!("Entry point: {entry:#010X}");
|
||||
println!("Entry point: {:#010X}", entry);
|
||||
}
|
||||
println!("\nSections:");
|
||||
println!("\t{: >10} | {: <10} | {: <10} | {: <10}", "Name", "Address", "Size", "File Off");
|
||||
@ -584,7 +572,6 @@ struct ModuleInfo<'a> {
|
||||
config: &'a ModuleConfig,
|
||||
symbols_cache: Option<FileReadInfo>,
|
||||
splits_cache: Option<FileReadInfo>,
|
||||
dep: Vec<Utf8NativePathBuf>,
|
||||
}
|
||||
|
||||
type ModuleMapByName<'a> = BTreeMap<String, ModuleInfo<'a>>;
|
||||
@ -824,29 +811,17 @@ struct AnalyzeResult {
|
||||
splits_cache: Option<FileReadInfo>,
|
||||
}
|
||||
|
||||
fn load_dol_module(
|
||||
config: &ModuleConfig,
|
||||
object_base: &ObjectBase,
|
||||
) -> Result<(ObjInfo, Utf8NativePathBuf)> {
|
||||
let object_path = object_base.join(&config.object);
|
||||
fn load_analyze_dol(config: &ProjectConfig, object_base: &ObjectBase) -> Result<AnalyzeResult> {
|
||||
let object_path = object_base.join(&config.base.object);
|
||||
log::debug!("Loading {}", object_path);
|
||||
let mut obj = {
|
||||
let mut file = object_base.open(&config.object)?;
|
||||
let mut file = object_base.open(&config.base.object)?;
|
||||
let data = file.map()?;
|
||||
if let Some(hash_str) = &config.hash {
|
||||
if let Some(hash_str) = &config.base.hash {
|
||||
verify_hash(data, hash_str)?;
|
||||
}
|
||||
process_dol(data, config.name())?
|
||||
process_dol(data, config.base.name())?
|
||||
};
|
||||
if config.clean_extab.unwrap_or(false) {
|
||||
log::debug!("Cleaning extab for {}", config.name());
|
||||
clean_extab(&mut obj, std::iter::empty())?;
|
||||
}
|
||||
Ok((obj, object_path))
|
||||
}
|
||||
|
||||
fn load_analyze_dol(config: &ProjectConfig, object_base: &ObjectBase) -> Result<AnalyzeResult> {
|
||||
let (mut obj, object_path) = load_dol_module(&config.base, object_base)?;
|
||||
let mut dep = vec![object_path];
|
||||
|
||||
if let Some(comment_version) = config.mw_comment_version {
|
||||
@ -973,7 +948,7 @@ fn split_write_obj(
|
||||
DirBuilder::new()
|
||||
.recursive(true)
|
||||
.create(out_dir)
|
||||
.with_context(|| format!("Failed to create out dir '{out_dir}'"))?;
|
||||
.with_context(|| format!("Failed to create out dir '{}'", out_dir))?;
|
||||
let obj_dir = out_dir.join("obj");
|
||||
let entry = if module.obj.kind == ObjKind::Executable {
|
||||
module.obj.entry.and_then(|e| {
|
||||
@ -993,19 +968,9 @@ fn split_write_obj(
|
||||
entry,
|
||||
extract: Vec::with_capacity(module.config.extract.len()),
|
||||
};
|
||||
let mut object_paths = BTreeMap::new();
|
||||
for (unit, split_obj) in module.obj.link_order.iter().zip(&split_objs) {
|
||||
let out_obj = write_elf(split_obj, config.export_all)?;
|
||||
let obj_path = obj_path_for_unit(&unit.name);
|
||||
let out_path = obj_dir.join(&obj_path);
|
||||
if let Some(existing) = object_paths.insert(obj_path, unit) {
|
||||
bail!(
|
||||
"Duplicate object path: {} and {} both resolve to {}",
|
||||
existing.name,
|
||||
unit.name,
|
||||
out_path,
|
||||
);
|
||||
}
|
||||
let out_path = obj_dir.join(obj_path_for_unit(&unit.name));
|
||||
out_config.units.push(OutputUnit {
|
||||
object: out_path.with_unix_encoding(),
|
||||
name: unit.name.clone(),
|
||||
@ -1049,8 +1014,7 @@ fn split_write_obj(
|
||||
|
||||
if header_kind != HeaderKind::None {
|
||||
if let Some(header) = &extract.header {
|
||||
let header_string =
|
||||
bin2c(symbol, section, data, header_kind, extract.rename.as_deref());
|
||||
let header_string = bin2c(symbol, section, data, header_kind);
|
||||
let out_path = base_dir.join("include").join(header.with_encoding());
|
||||
if let Some(parent) = out_path.parent() {
|
||||
DirBuilder::new().recursive(true).create(parent)?;
|
||||
@ -1062,7 +1026,6 @@ fn split_write_obj(
|
||||
// Copy to output config
|
||||
out_config.extract.push(OutputExtract {
|
||||
symbol: symbol.name.clone(),
|
||||
rename: extract.rename.clone(),
|
||||
binary: extract.binary.clone(),
|
||||
header: extract.header.clone(),
|
||||
header_type: header_kind.to_string(),
|
||||
@ -1074,10 +1037,9 @@ fn split_write_obj(
|
||||
// Generate ldscript.lcf
|
||||
let ldscript_template = if let Some(template_path) = &module.config.ldscript_template {
|
||||
let template_path = template_path.with_encoding();
|
||||
let template = fs::read_to_string(&template_path)
|
||||
.with_context(|| format!("Failed to read linker script template '{template_path}'"))?;
|
||||
module.dep.push(template_path);
|
||||
Some(template)
|
||||
Some(fs::read_to_string(&template_path).with_context(|| {
|
||||
format!("Failed to read linker script template '{}'", template_path)
|
||||
})?)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
@ -1093,7 +1055,8 @@ fn split_write_obj(
|
||||
let out_path = asm_dir.join(asm_path_for_unit(&unit.name));
|
||||
|
||||
let mut w = buf_writer(&out_path)?;
|
||||
write_asm(&mut w, split_obj).with_context(|| format!("Failed to write {out_path}"))?;
|
||||
write_asm(&mut w, split_obj)
|
||||
.with_context(|| format!("Failed to write {}", out_path))?;
|
||||
w.flush()?;
|
||||
}
|
||||
}
|
||||
@ -1110,7 +1073,7 @@ fn write_if_changed(path: &Utf8NativePath, contents: &[u8]) -> Result<()> {
|
||||
return Ok(());
|
||||
}
|
||||
}
|
||||
fs::write(path, contents).with_context(|| format!("Failed to write file '{path}'"))?;
|
||||
fs::write(path, contents).with_context(|| format!("Failed to write file '{}'", path))?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@ -1264,7 +1227,6 @@ fn split(args: SplitArgs) -> Result<()> {
|
||||
config: &config.base,
|
||||
symbols_cache: result.symbols_cache,
|
||||
splits_cache: result.splits_cache,
|
||||
dep: Default::default(),
|
||||
}
|
||||
};
|
||||
let mut function_count = dol.obj.symbols.by_kind(ObjSymbolKind::Function).count();
|
||||
@ -1279,7 +1241,6 @@ fn split(args: SplitArgs) -> Result<()> {
|
||||
config: &config.modules[idx],
|
||||
symbols_cache: result.symbols_cache,
|
||||
splits_cache: result.splits_cache,
|
||||
dep: Default::default(),
|
||||
}),
|
||||
Entry::Occupied(_) => bail!("Duplicate module name {}", result.obj.name),
|
||||
};
|
||||
@ -1461,10 +1422,6 @@ fn split(args: SplitArgs) -> Result<()> {
|
||||
}
|
||||
|
||||
// Write dep file
|
||||
dep.extend(dol.dep);
|
||||
for module in modules.into_values() {
|
||||
dep.extend(module.dep);
|
||||
}
|
||||
{
|
||||
let dep_path = args.out_dir.join("dep");
|
||||
let mut dep_file = buf_writer(&dep_path)?;
|
||||
@ -1634,49 +1591,42 @@ fn validate(obj: &ObjInfo, elf_file: &Utf8NativePath, state: &AnalyzerState) ->
|
||||
|
||||
/// Check if two symbols' names match, allowing for differences in compiler-generated names,
|
||||
/// like @1234 and @5678, or init$1234 and init$5678.
|
||||
fn symbol_name_fuzzy_eq(a: &str, b: &str) -> bool {
|
||||
if a == b {
|
||||
fn symbol_name_fuzzy_eq(a: &ObjSymbol, b: &ObjSymbol) -> bool {
|
||||
if a.name == b.name {
|
||||
return true;
|
||||
}
|
||||
// Match e.g. @1234 and @5678
|
||||
if a.starts_with('@') && b.starts_with('@') {
|
||||
if a.name.starts_with('@') && b.name.starts_with('@') {
|
||||
return true;
|
||||
}
|
||||
// Match e.g. init$1234 and init$5678
|
||||
if let (Some(a_dollar), Some(b_dollar)) = (a.rfind('$'), b.rfind('$')) {
|
||||
if a[..a_dollar] == b[..b_dollar] {
|
||||
if let (Some(a_dollar), Some(b_dollar)) = (a.name.rfind('$'), b.name.rfind('$')) {
|
||||
if a.name[..a_dollar] == b.name[..b_dollar] {
|
||||
if let (Ok(_), Ok(_)) =
|
||||
(a[a_dollar + 1..].parse::<u32>(), b[b_dollar + 1..].parse::<u32>())
|
||||
(a.name[a_dollar + 1..].parse::<u32>(), b.name[b_dollar + 1..].parse::<u32>())
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
// Match e.g. symbol and symbol_80123456 (globalized symbol)
|
||||
if let Some(a_under) = a.rfind('_') {
|
||||
if &a[..a_under] == b && is_hex(&a[a_under + 1..]) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
if let Some(b_under) = b.rfind('_') {
|
||||
if a == &b[..b_under] && is_hex(&b[b_under + 1..]) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
false
|
||||
}
|
||||
|
||||
fn is_hex(s: &str) -> bool {
|
||||
s.chars().all(|c| c.is_ascii_digit() || matches!(c, 'a'..='f' | 'A'..='F'))
|
||||
}
|
||||
|
||||
fn diff(args: DiffArgs) -> Result<()> {
|
||||
log::info!("Loading {}", args.config);
|
||||
let mut config_file = open_file(&args.config, true)?;
|
||||
let config: ProjectConfig = serde_yaml::from_reader(config_file.as_mut())?;
|
||||
let object_base = find_object_base(&config)?;
|
||||
|
||||
let (mut obj, _object_path) = load_dol_module(&config.base, &object_base)?;
|
||||
log::info!("Loading {}", object_base.join(&config.base.object));
|
||||
let mut obj = {
|
||||
let mut file = object_base.open(&config.base.object)?;
|
||||
let data = file.map()?;
|
||||
if let Some(hash_str) = &config.base.hash {
|
||||
verify_hash(data, hash_str)?;
|
||||
}
|
||||
process_dol(data, config.base.name())?
|
||||
};
|
||||
|
||||
if let Some(symbols_path) = &config.base.symbols {
|
||||
apply_symbols_file(&symbols_path.with_encoding(), &mut obj)?;
|
||||
@ -1706,7 +1656,7 @@ fn diff(args: DiffArgs) -> Result<()> {
|
||||
});
|
||||
let mut found = false;
|
||||
if let Some((_, linked_sym)) = linked_sym {
|
||||
if symbol_name_fuzzy_eq(&linked_sym.name, &orig_sym.name) {
|
||||
if symbol_name_fuzzy_eq(linked_sym, orig_sym) {
|
||||
if linked_sym.size != orig_sym.size &&
|
||||
// TODO validate common symbol sizes
|
||||
// (need to account for inflation bug)
|
||||
@ -1722,7 +1672,7 @@ fn diff(args: DiffArgs) -> Result<()> {
|
||||
);
|
||||
}
|
||||
found = true;
|
||||
} else if linked_sym.kind == orig_sym.kind {
|
||||
} else if linked_sym.kind == orig_sym.kind && linked_sym.size == orig_sym.size {
|
||||
// Fuzzy match
|
||||
let orig_data = orig_section.data_range(
|
||||
orig_sym.address as u32,
|
||||
@ -1732,12 +1682,7 @@ fn diff(args: DiffArgs) -> Result<()> {
|
||||
linked_sym.address as u32,
|
||||
linked_sym.address as u32 + linked_sym.size as u32,
|
||||
)?;
|
||||
let len = orig_data.len().min(linked_data.len());
|
||||
if orig_data[..len] == linked_data[..len]
|
||||
// Ignore padding differences
|
||||
&& orig_data[len..].iter().all(|&b| b == 0)
|
||||
&& linked_data[len..].iter().all(|&b| b == 0)
|
||||
{
|
||||
if orig_data == linked_data {
|
||||
found = true;
|
||||
}
|
||||
}
|
||||
@ -1801,11 +1746,7 @@ fn diff(args: DiffArgs) -> Result<()> {
|
||||
linked_sym.address as u32,
|
||||
linked_sym.address as u32 + linked_sym.size as u32,
|
||||
)?;
|
||||
let len = orig_data.len().min(linked_data.len());
|
||||
if orig_data[..len] != linked_data[..len]
|
||||
|| orig_data[len..].iter().any(|&b| b != 0)
|
||||
|| linked_data[len..].iter().any(|&b| b != 0)
|
||||
{
|
||||
if orig_data != linked_data {
|
||||
log::error!(
|
||||
"Data mismatch for {} (type {:?}, size {:#X}) at {:#010X}",
|
||||
orig_sym.name,
|
||||
@ -1845,15 +1786,6 @@ fn diff(args: DiffArgs) -> Result<()> {
|
||||
}
|
||||
|
||||
std::process::exit(1);
|
||||
} else if orig_data.len() != linked_data.len() {
|
||||
log::error!(
|
||||
"Size mismatch for {} (type {:?}) at {:#010X}: Expected {:#X}, found {:#X}",
|
||||
orig_sym.name,
|
||||
orig_sym.kind,
|
||||
orig_sym.address,
|
||||
orig_data.len(),
|
||||
linked_data.len()
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@ -1861,38 +1793,21 @@ fn diff(args: DiffArgs) -> Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn are_local_anonymous_names_similar<'a>(left: &'a ObjSymbol, right: &'a ObjSymbol) -> bool {
|
||||
if left.flags.scope() != ObjSymbolScope::Local || right.flags.scope() != ObjSymbolScope::Local {
|
||||
return false;
|
||||
}
|
||||
|
||||
let is_at_symbol =
|
||||
|name: &str| name.starts_with('@') && name[1..].chars().all(|c| c.is_numeric());
|
||||
|
||||
if is_at_symbol(&left.name) && is_at_symbol(&right.name) {
|
||||
// consider e.g. @8280 -> @8536 equal
|
||||
return true;
|
||||
}
|
||||
|
||||
let split_dollar_symbol = |name: &'a str| -> Option<&'a str> {
|
||||
name.rsplit_once('$')
|
||||
.and_then(|(prefix, suffix)| suffix.chars().all(|c| c.is_numeric()).then_some(prefix))
|
||||
};
|
||||
|
||||
// consider e.g. __arraydtor$3926 -> __arraydtor$7669 equal
|
||||
match (split_dollar_symbol(&left.name), split_dollar_symbol(&right.name)) {
|
||||
(Some(left_prefix), Some(right_prefix)) => left_prefix == right_prefix,
|
||||
_ => false,
|
||||
}
|
||||
}
|
||||
|
||||
fn apply(args: ApplyArgs) -> Result<()> {
|
||||
log::info!("Loading {}", args.config);
|
||||
let mut config_file = open_file(&args.config, true)?;
|
||||
let config: ProjectConfig = serde_yaml::from_reader(config_file.as_mut())?;
|
||||
let object_base = find_object_base(&config)?;
|
||||
|
||||
let (mut obj, _object_path) = load_dol_module(&config.base, &object_base)?;
|
||||
log::info!("Loading {}", object_base.join(&config.base.object));
|
||||
let mut obj = {
|
||||
let mut file = object_base.open(&config.base.object)?;
|
||||
let data = file.map()?;
|
||||
if let Some(hash_str) = &config.base.hash {
|
||||
verify_hash(data, hash_str)?;
|
||||
}
|
||||
process_dol(data, config.base.name())?
|
||||
};
|
||||
|
||||
let Some(symbols_path) = &config.base.symbols else {
|
||||
bail!("No symbols file specified in config");
|
||||
@ -1928,9 +1843,7 @@ fn apply(args: ApplyArgs) -> Result<()> {
|
||||
let mut updated_sym = orig_sym.clone();
|
||||
let is_globalized = linked_sym.name.ends_with(&format!("_{:08X}", linked_sym.address));
|
||||
if (is_globalized && !linked_sym.name.starts_with(&orig_sym.name))
|
||||
|| (!is_globalized
|
||||
&& (linked_sym.name != orig_sym.name
|
||||
&& (args.full || !are_local_anonymous_names_similar(linked_sym, orig_sym))))
|
||||
|| (!is_globalized && linked_sym.name != orig_sym.name)
|
||||
{
|
||||
log::info!(
|
||||
"Changing name of {} (type {:?}) to {}",
|
||||
@ -2166,7 +2079,7 @@ impl ObjectBase {
|
||||
}
|
||||
base.join(path.with_encoding())
|
||||
}
|
||||
ObjectBase::Vfs(base, _) => Utf8NativePathBuf::from(format!("{base}:{path}")),
|
||||
ObjectBase::Vfs(base, _) => Utf8NativePathBuf::from(format!("{}:{}", base, path)),
|
||||
}
|
||||
}
|
||||
|
||||
@ -2183,7 +2096,7 @@ impl ObjectBase {
|
||||
}
|
||||
ObjectBase::Vfs(vfs_path, vfs) => {
|
||||
open_file_with_fs(vfs.clone(), &path.with_encoding(), true)
|
||||
.with_context(|| format!("Using disc image {vfs_path}"))
|
||||
.with_context(|| format!("Using disc image {}", vfs_path))
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -2201,36 +2114,30 @@ pub fn find_object_base(config: &ProjectConfig) -> Result<ObjectBase> {
|
||||
if let Some(base) = &config.object_base {
|
||||
let base = base.with_encoding();
|
||||
// Search for disc images in the object base directory
|
||||
for result in fs::read_dir(&base).with_context(|| format!("Reading directory {base}"))? {
|
||||
let entry = result.with_context(|| format!("Reading entry in directory {base}"))?;
|
||||
for result in fs::read_dir(&base).with_context(|| format!("Reading directory {}", base))? {
|
||||
let entry = result.with_context(|| format!("Reading entry in directory {}", base))?;
|
||||
let Ok(path) = check_path_buf(entry.path()) else {
|
||||
log::warn!("Path is not valid UTF-8: {:?}", entry.path());
|
||||
continue;
|
||||
};
|
||||
let file_type =
|
||||
entry.file_type().with_context(|| format!("Getting file type for {path}"))?;
|
||||
entry.file_type().with_context(|| format!("Getting file type for {}", path))?;
|
||||
let is_file = if file_type.is_symlink() {
|
||||
// Also traverse symlinks to files
|
||||
fs::metadata(&path)
|
||||
.with_context(|| format!("Getting metadata for {path}"))?
|
||||
.with_context(|| format!("Getting metadata for {}", path))?
|
||||
.is_file()
|
||||
} else {
|
||||
file_type.is_file()
|
||||
};
|
||||
if is_file {
|
||||
let mut file = open_file(&path, false)?;
|
||||
let format = detect(file.as_mut())
|
||||
.with_context(|| format!("Detecting file type for {path}"))?;
|
||||
match format {
|
||||
FileFormat::Archive(ArchiveKind::Disc(format)) => {
|
||||
let fs = open_fs(file, ArchiveKind::Disc(format))?;
|
||||
return Ok(ObjectBase::Vfs(path, fs));
|
||||
}
|
||||
FileFormat::Archive(ArchiveKind::Wad) => {
|
||||
let fs = open_fs(file, ArchiveKind::Wad)?;
|
||||
return Ok(ObjectBase::Vfs(path, fs));
|
||||
}
|
||||
_ => {}
|
||||
let format = nodtool::nod::Disc::detect(file.as_mut())
|
||||
.with_context(|| format!("Detecting file type for {}", path))?;
|
||||
if let Some(format) = format {
|
||||
file.rewind()?;
|
||||
let fs = open_fs(file, ArchiveKind::Disc(format))?;
|
||||
return Ok(ObjectBase::Vfs(path, fs));
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -2249,7 +2156,7 @@ fn extract_objects(config: &ProjectConfig, object_base: &ObjectBase) -> Result<U
|
||||
{
|
||||
let target_path = extracted_path(&target_dir, &config.base.object);
|
||||
if !fs::exists(&target_path)
|
||||
.with_context(|| format!("Failed to check path '{target_path}'"))?
|
||||
.with_context(|| format!("Failed to check path '{}'", target_path))?
|
||||
{
|
||||
object_paths.push((&config.base.object, config.base.hash.as_deref(), target_path));
|
||||
}
|
||||
@ -2257,7 +2164,7 @@ fn extract_objects(config: &ProjectConfig, object_base: &ObjectBase) -> Result<U
|
||||
if let Some(selfile) = &config.selfile {
|
||||
let target_path = extracted_path(&target_dir, selfile);
|
||||
if !fs::exists(&target_path)
|
||||
.with_context(|| format!("Failed to check path '{target_path}'"))?
|
||||
.with_context(|| format!("Failed to check path '{}'", target_path))?
|
||||
{
|
||||
object_paths.push((selfile, config.selfile_hash.as_deref(), target_path));
|
||||
}
|
||||
@ -2265,7 +2172,7 @@ fn extract_objects(config: &ProjectConfig, object_base: &ObjectBase) -> Result<U
|
||||
for module_config in &config.modules {
|
||||
let target_path = extracted_path(&target_dir, &module_config.object);
|
||||
if !fs::exists(&target_path)
|
||||
.with_context(|| format!("Failed to check path '{target_path}'"))?
|
||||
.with_context(|| format!("Failed to check path '{}'", target_path))?
|
||||
{
|
||||
object_paths.push((&module_config.object, module_config.hash.as_deref(), target_path));
|
||||
}
|
||||
@ -2284,12 +2191,12 @@ fn extract_objects(config: &ProjectConfig, object_base: &ObjectBase) -> Result<U
|
||||
let mut file = object_base.open(source_path)?;
|
||||
if let Some(parent) = target_path.parent() {
|
||||
fs::create_dir_all(parent)
|
||||
.with_context(|| format!("Failed to create directory '{parent}'"))?;
|
||||
.with_context(|| format!("Failed to create directory '{}'", parent))?;
|
||||
}
|
||||
let mut out = fs::File::create(&target_path)
|
||||
.with_context(|| format!("Failed to create file '{target_path}'"))?;
|
||||
.with_context(|| format!("Failed to create file '{}'", target_path))?;
|
||||
let hash_bytes = buf_copy_with_hash(&mut file, &mut out)
|
||||
.with_context(|| format!("Failed to extract file '{target_path}'"))?;
|
||||
.with_context(|| format!("Failed to extract file '{}'", target_path))?;
|
||||
if let Some(hash) = hash {
|
||||
check_hash_str(hash_bytes, hash).with_context(|| {
|
||||
format!("Source file failed verification: '{}'", object_base.join(source_path))
|
||||
@ -2318,21 +2225,3 @@ fn extracted_path(target_dir: &Utf8NativePath, path: &Utf8UnixPath) -> Utf8Nativ
|
||||
}
|
||||
target_path
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_symbol_name_fuzzy_eq() {
|
||||
assert!(symbol_name_fuzzy_eq("symbol", "symbol"));
|
||||
assert!(symbol_name_fuzzy_eq("@1234", "@5678"));
|
||||
assert!(symbol_name_fuzzy_eq("symbol$1234", "symbol$5678"));
|
||||
assert!(symbol_name_fuzzy_eq("symbol", "symbol_80123456"));
|
||||
assert!(symbol_name_fuzzy_eq("symbol_80123456", "symbol"));
|
||||
assert!(!symbol_name_fuzzy_eq("symbol", "symbol2"));
|
||||
assert!(!symbol_name_fuzzy_eq("symbol@1234", "symbol@5678"));
|
||||
assert!(!symbol_name_fuzzy_eq("symbol", "symbol_80123456_"));
|
||||
assert!(!symbol_name_fuzzy_eq("symbol_80123456_", "symbol"));
|
||||
}
|
||||
}
|
||||
|
@ -104,16 +104,16 @@ fn dump(args: DumpArgs) -> Result<()> {
|
||||
// TODO make a basename method
|
||||
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!("{name}.txt"));
|
||||
let file_path = out_path.join(format!("{}.txt", name));
|
||||
let mut file = buf_writer(&file_path)?;
|
||||
dump_debug_section(&args, &mut file, &obj_file, debug_section)?;
|
||||
file.flush()?;
|
||||
} else if args.no_color {
|
||||
println!("\n// File {name}:");
|
||||
println!("\n// File {}:", name);
|
||||
dump_debug_section(&args, &mut stdout(), &obj_file, debug_section)?;
|
||||
} else {
|
||||
let mut writer = HighlightWriter::new(syntax_set.clone(), syntax.clone(), theme);
|
||||
writeln!(writer, "\n// File {name}:")?;
|
||||
writeln!(writer, "\n// File {}:", name)?;
|
||||
dump_debug_section(&args, &mut writer, &obj_file, debug_section)?;
|
||||
}
|
||||
}
|
||||
@ -209,25 +209,26 @@ where
|
||||
}
|
||||
writeln!(w, "\n/*\n Compile unit: {}", unit.name)?;
|
||||
if let Some(producer) = unit.producer {
|
||||
writeln!(w, " Producer: {producer}")?;
|
||||
writeln!(w, " Producer: {}", producer)?;
|
||||
}
|
||||
if let Some(comp_dir) = unit.comp_dir {
|
||||
writeln!(w, " Compile directory: {comp_dir}")?;
|
||||
writeln!(w, " Compile directory: {}", comp_dir)?;
|
||||
}
|
||||
if let Some(language) = unit.language {
|
||||
writeln!(w, " Language: {language}")?;
|
||||
writeln!(w, " Language: {}", language)?;
|
||||
}
|
||||
if let (Some(start), Some(end)) = (unit.start_address, unit.end_address) {
|
||||
writeln!(w, " Code range: {start:#010X} -> {end:#010X}")?;
|
||||
writeln!(w, " Code range: {:#010X} -> {:#010X}", start, end)?;
|
||||
}
|
||||
if let Some(gcc_srcfile_name_offset) = unit.gcc_srcfile_name_offset {
|
||||
writeln!(
|
||||
w,
|
||||
" GCC Source File Name Offset: {gcc_srcfile_name_offset:#010X}"
|
||||
" GCC Source File Name Offset: {:#010X}",
|
||||
gcc_srcfile_name_offset
|
||||
)?;
|
||||
}
|
||||
if let Some(gcc_srcinfo_offset) = unit.gcc_srcinfo_offset {
|
||||
writeln!(w, " GCC Source Info Offset: {gcc_srcinfo_offset:#010X}")?;
|
||||
writeln!(w, " GCC Source Info Offset: {:#010X}", gcc_srcinfo_offset)?;
|
||||
}
|
||||
writeln!(w, "*/")?;
|
||||
|
||||
@ -268,7 +269,7 @@ where
|
||||
continue;
|
||||
}
|
||||
match tag_type_string(&info, &typedefs, &tag_type, child.is_erased) {
|
||||
Ok(s) => writeln!(w, "{s}")?,
|
||||
Ok(s) => writeln!(w, "{}", s)?,
|
||||
Err(e) => {
|
||||
log::error!(
|
||||
"Failed to emit tag {:X} (unit {}): {}",
|
||||
@ -359,7 +360,7 @@ fn blend_fg_color(fg: Color, bg: Color) -> Color {
|
||||
|
||||
impl Write for HighlightWriter<'_> {
|
||||
fn write(&mut self, buf: &[u8]) -> std::io::Result<usize> {
|
||||
let str = from_utf8(buf).map_err(std::io::Error::other)?;
|
||||
let str = from_utf8(buf).map_err(|e| std::io::Error::new(std::io::ErrorKind::Other, e))?;
|
||||
for s in str.split_inclusive('\n') {
|
||||
self.line.push_str(s);
|
||||
if self.line.ends_with('\n') {
|
||||
@ -376,7 +377,7 @@ impl Write for HighlightWriter<'_> {
|
||||
let ops = self
|
||||
.parse_state
|
||||
.parse_line(&self.line, &self.syntax_set)
|
||||
.map_err(std::io::Error::other)?;
|
||||
.map_err(|e| std::io::Error::new(std::io::ErrorKind::Other, e))?;
|
||||
let iter = HighlightIterator::new(
|
||||
&mut self.highlight_state,
|
||||
&ops[..],
|
||||
|
@ -1,11 +1,11 @@
|
||||
use std::{
|
||||
collections::{btree_map, BTreeMap, HashMap},
|
||||
collections::{btree_map, hash_map, BTreeMap, HashMap},
|
||||
fs,
|
||||
fs::DirBuilder,
|
||||
io::{Cursor, Write},
|
||||
};
|
||||
|
||||
use anyhow::{anyhow, ensure, Context, Result};
|
||||
use anyhow::{anyhow, bail, ensure, Context, Result};
|
||||
use argp::FromArgs;
|
||||
use objdiff_core::obj::split_meta::{SplitMeta, SPLITMETA_SECTION};
|
||||
use object::{
|
||||
@ -14,7 +14,7 @@ use object::{
|
||||
FileFlags, Object, ObjectSection, ObjectSymbol, RelocationTarget, SectionFlags, SectionIndex,
|
||||
SectionKind, SymbolFlags, SymbolIndex, SymbolKind, SymbolScope, SymbolSection,
|
||||
};
|
||||
use typed_path::Utf8NativePathBuf;
|
||||
use typed_path::{Utf8NativePath, Utf8NativePathBuf};
|
||||
|
||||
use crate::{
|
||||
obj::ObjKind,
|
||||
@ -22,7 +22,7 @@ use crate::{
|
||||
asm::write_asm,
|
||||
comment::{CommentSym, MWComment},
|
||||
config::{write_splits_file, write_symbols_file},
|
||||
elf::process_elf,
|
||||
elf::{process_elf, write_elf},
|
||||
file::{buf_writer, process_rsp},
|
||||
path::native_path,
|
||||
reader::{Endian, FromReader},
|
||||
@ -47,6 +47,7 @@ enum SubCommand {
|
||||
Disasm(DisasmArgs),
|
||||
Fixup(FixupArgs),
|
||||
Signatures(SignaturesArgs),
|
||||
Split(SplitArgs),
|
||||
Info(InfoArgs),
|
||||
}
|
||||
|
||||
@ -74,6 +75,18 @@ pub struct FixupArgs {
|
||||
out_file: Utf8NativePathBuf,
|
||||
}
|
||||
|
||||
#[derive(FromArgs, PartialEq, Eq, Debug)]
|
||||
/// Splits an executable ELF into relocatable objects.
|
||||
#[argp(subcommand, name = "split")]
|
||||
pub struct SplitArgs {
|
||||
#[argp(positional, from_str_fn(native_path))]
|
||||
/// input file
|
||||
in_file: Utf8NativePathBuf,
|
||||
#[argp(positional, from_str_fn(native_path))]
|
||||
/// output directory
|
||||
out_dir: Utf8NativePathBuf,
|
||||
}
|
||||
|
||||
#[derive(FromArgs, PartialEq, Eq, Debug)]
|
||||
/// Generates configuration files from an executable ELF.
|
||||
#[argp(subcommand, name = "config")]
|
||||
@ -115,6 +128,7 @@ pub fn run(args: Args) -> Result<()> {
|
||||
SubCommand::Config(c_args) => config(c_args),
|
||||
SubCommand::Disasm(c_args) => disasm(c_args),
|
||||
SubCommand::Fixup(c_args) => fixup(c_args),
|
||||
SubCommand::Split(c_args) => split(c_args),
|
||||
SubCommand::Signatures(c_args) => signatures(c_args),
|
||||
SubCommand::Info(c_args) => info(c_args),
|
||||
}
|
||||
@ -145,15 +159,14 @@ fn disasm(args: DisasmArgs) -> Result<()> {
|
||||
|
||||
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_name = file_stem_from_unit(&unit.name);
|
||||
let out_path = asm_dir.join(format!("{out_name}.s"));
|
||||
let out_path = asm_dir.join(file_name_from_unit(&unit.name, ".s"));
|
||||
log::info!("Writing {}", out_path);
|
||||
|
||||
let mut w = buf_writer(&out_path)?;
|
||||
write_asm(&mut w, split_obj)?;
|
||||
w.flush()?;
|
||||
|
||||
writeln!(files_out, "{out_name}.o")?;
|
||||
writeln!(files_out, "{}", file_name_from_unit(&unit.name, ".o"))?;
|
||||
}
|
||||
files_out.flush()?;
|
||||
}
|
||||
@ -166,7 +179,38 @@ fn disasm(args: DisasmArgs) -> Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn file_stem_from_unit(str: &str) -> String {
|
||||
fn split(args: SplitArgs) -> Result<()> {
|
||||
let obj = process_elf(&args.in_file)?;
|
||||
ensure!(obj.kind == ObjKind::Executable, "Can only split executable objects");
|
||||
|
||||
let mut file_map = HashMap::<String, Vec<u8>>::new();
|
||||
|
||||
let split_objs = split_obj(&obj, None)?;
|
||||
for (unit, split_obj) in obj.link_order.iter().zip(&split_objs) {
|
||||
let out_obj = write_elf(split_obj, false)?;
|
||||
match file_map.entry(unit.name.clone()) {
|
||||
hash_map::Entry::Vacant(e) => e.insert(out_obj),
|
||||
hash_map::Entry::Occupied(_) => bail!("Duplicate file {}", unit.name),
|
||||
};
|
||||
}
|
||||
|
||||
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)?;
|
||||
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))?;
|
||||
}
|
||||
rsp_file.flush()?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn file_name_from_unit(str: &str, suffix: &str) -> String {
|
||||
let str = str.strip_suffix(ASM_SUFFIX).unwrap_or(str);
|
||||
let str = str.strip_prefix("C:").unwrap_or(str);
|
||||
let str = str.strip_prefix("D:").unwrap_or(str);
|
||||
@ -178,7 +222,8 @@ fn file_stem_from_unit(str: &str) -> String {
|
||||
.or_else(|| str.strip_suffix(".o"))
|
||||
.unwrap_or(str);
|
||||
let str = str.replace('\\', "/");
|
||||
str.strip_prefix('/').unwrap_or(&str).to_string()
|
||||
let str = str.strip_prefix('/').unwrap_or(&str);
|
||||
format!("{str}{suffix}")
|
||||
}
|
||||
|
||||
const ASM_SUFFIX: &str = " (asm)";
|
||||
@ -402,7 +447,7 @@ fn signatures(args: SignaturesArgs) -> Result<()> {
|
||||
Ok(Some(signature)) => signature,
|
||||
Ok(None) => continue,
|
||||
Err(e) => {
|
||||
eprintln!("Failed: {e:?}");
|
||||
eprintln!("Failed: {:?}", e);
|
||||
continue;
|
||||
}
|
||||
};
|
||||
@ -545,13 +590,13 @@ fn info(args: InfoArgs) -> Result<()> {
|
||||
.context("While reading .note.split section")?;
|
||||
println!("\nSplit metadata (.note.split):");
|
||||
if let Some(generator) = &meta.generator {
|
||||
println!("\tGenerator: {generator}");
|
||||
println!("\tGenerator: {}", generator);
|
||||
}
|
||||
if let Some(module_name) = &meta.module_name {
|
||||
println!("\tModule name: {module_name}");
|
||||
println!("\tModule name: {}", module_name);
|
||||
}
|
||||
if let Some(module_id) = meta.module_id {
|
||||
println!("\tModule ID: {module_id}");
|
||||
println!("\tModule ID: {}", module_id);
|
||||
}
|
||||
if let Some(virtual_addresses) = &meta.virtual_addresses {
|
||||
println!("\tVirtual addresses:");
|
||||
|
@ -6,20 +6,16 @@ use object::{Architecture, Endianness, Object, ObjectKind, ObjectSection, Sectio
|
||||
use typed_path::Utf8NativePathBuf;
|
||||
|
||||
use crate::{
|
||||
util::{
|
||||
dol::{process_dol, write_dol},
|
||||
file::buf_writer,
|
||||
path::native_path,
|
||||
},
|
||||
util::{file::buf_writer, path::native_path},
|
||||
vfs::open_file,
|
||||
};
|
||||
|
||||
#[derive(FromArgs, PartialEq, Eq, Debug)]
|
||||
/// Converts an ELF, ALF, or BootStage file to a DOL file.
|
||||
/// Converts an ELF file to a DOL file.
|
||||
#[argp(subcommand, name = "elf2dol")]
|
||||
pub struct Args {
|
||||
#[argp(positional, from_str_fn(native_path))]
|
||||
/// path to input ELF, ALF or BootStage file
|
||||
/// path to input ELF
|
||||
elf_file: Utf8NativePathBuf,
|
||||
#[argp(positional, from_str_fn(native_path))]
|
||||
/// path to output DOL
|
||||
@ -52,12 +48,7 @@ const MAX_DATA_SECTIONS: usize = 11;
|
||||
|
||||
pub fn run(args: Args) -> Result<()> {
|
||||
let mut file = open_file(&args.elf_file, true)?;
|
||||
let data = file.map()?;
|
||||
if data.len() >= 4 && data[0..4] != object::elf::ELFMAG {
|
||||
return convert_dol_like(args, data);
|
||||
}
|
||||
|
||||
let obj_file = object::read::File::parse(data)?;
|
||||
let obj_file = object::read::File::parse(file.map()?)?;
|
||||
match obj_file.architecture() {
|
||||
Architecture::PowerPc => {}
|
||||
arch => bail!("Unexpected architecture: {arch:?}"),
|
||||
@ -162,14 +153,6 @@ pub fn run(args: Args) -> Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Converts a DOL-like format (ALF or BootStage) to a DOL file.
|
||||
fn convert_dol_like(args: Args, data: &[u8]) -> Result<()> {
|
||||
let obj = process_dol(data, "")?;
|
||||
let mut out = buf_writer(&args.dol_file)?;
|
||||
write_dol(&obj, &mut out)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[inline]
|
||||
const fn align32(x: u32) -> u32 { (x + 31) & !31 }
|
||||
|
||||
|
@ -1,78 +0,0 @@
|
||||
use std::io::Write;
|
||||
|
||||
use anyhow::{Context, Result};
|
||||
use argp::FromArgs;
|
||||
use typed_path::Utf8NativePathBuf;
|
||||
|
||||
use crate::{
|
||||
util,
|
||||
util::{
|
||||
dol::{process_dol, write_dol},
|
||||
elf::{is_elf_file, process_elf, write_elf},
|
||||
file::buf_writer,
|
||||
path::native_path,
|
||||
},
|
||||
vfs::open_file,
|
||||
};
|
||||
|
||||
#[derive(FromArgs, PartialEq, Debug)]
|
||||
/// Commands for processing extab (exception table) data.
|
||||
#[argp(subcommand, name = "extab")]
|
||||
pub struct Args {
|
||||
#[argp(subcommand)]
|
||||
command: SubCommand,
|
||||
}
|
||||
|
||||
#[derive(FromArgs, PartialEq, Debug)]
|
||||
#[argp(subcommand)]
|
||||
enum SubCommand {
|
||||
Clean(CleanArgs),
|
||||
}
|
||||
|
||||
#[derive(FromArgs, PartialEq, Eq, Debug)]
|
||||
/// Rewrites extab data in a DOL or ELF file, replacing any uninitialized padding bytes.
|
||||
#[argp(subcommand, name = "clean")]
|
||||
pub struct CleanArgs {
|
||||
#[argp(positional, from_str_fn(native_path))]
|
||||
/// Path to input file
|
||||
input: Utf8NativePathBuf,
|
||||
#[argp(positional, from_str_fn(native_path))]
|
||||
/// Path to output file
|
||||
output: Utf8NativePathBuf,
|
||||
#[argp(option, short = 'p')]
|
||||
/// Data to replace padding bytes with, encoded as a hexadecimal string. If not specified, padding bytes will be zeroed instead.
|
||||
padding: Option<String>,
|
||||
}
|
||||
|
||||
pub fn run(args: Args) -> Result<()> {
|
||||
match args.command {
|
||||
SubCommand::Clean(clean_args) => clean_extab(clean_args),
|
||||
}
|
||||
}
|
||||
|
||||
fn clean_extab(args: CleanArgs) -> Result<()> {
|
||||
let is_elf = is_elf_file(&args.input)?;
|
||||
let mut obj = if is_elf {
|
||||
process_elf(&args.input)?
|
||||
} else {
|
||||
let mut file = open_file(&args.input, true)?;
|
||||
let name = args.input.file_stem().unwrap_or_default();
|
||||
process_dol(file.map()?, name)?
|
||||
};
|
||||
let padding: Vec<u8> = match args.padding {
|
||||
None => Vec::new(),
|
||||
Some(padding_str) => {
|
||||
hex::decode(padding_str).context("Failed to decode padding bytes from hex")?
|
||||
}
|
||||
};
|
||||
let num_cleaned = util::extab::clean_extab(&mut obj, padding.iter().copied())?;
|
||||
tracing::debug!("Cleaned {num_cleaned} extab symbols");
|
||||
let mut out = buf_writer(&args.output)?;
|
||||
if is_elf {
|
||||
let data = write_elf(&obj, false)?;
|
||||
out.write_all(&data).context("Failed to write ELF")?;
|
||||
} else {
|
||||
write_dol(&obj, &mut out).context("Failed to write DOL")?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
@ -175,7 +175,7 @@ fn symbol(args: SymbolArgs) -> Result<()> {
|
||||
if let Some(vec) = entries.unit_references.get_vec(&symbol_ref) {
|
||||
println!("\nGenerated in TUs:");
|
||||
for x in vec {
|
||||
println!(">>> {x}");
|
||||
println!(">>> {}", x);
|
||||
}
|
||||
}
|
||||
println!("\n");
|
||||
|
@ -6,7 +6,6 @@ pub mod dol;
|
||||
pub mod dwarf;
|
||||
pub mod elf;
|
||||
pub mod elf2dol;
|
||||
pub mod extab;
|
||||
pub mod map;
|
||||
pub mod nlzss;
|
||||
pub mod rarc;
|
||||
@ -15,6 +14,5 @@ pub mod rso;
|
||||
pub mod shasum;
|
||||
pub mod u8_arc;
|
||||
pub mod vfs;
|
||||
pub mod wad;
|
||||
pub mod yay0;
|
||||
pub mod yaz0;
|
||||
|
@ -59,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}'"))?;
|
||||
.with_context(|| format!("Failed to write '{}'", out_path))?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
@ -104,9 +104,6 @@ pub struct MakeArgs {
|
||||
#[argp(option, short = 'n')]
|
||||
/// (optional) module names
|
||||
names: Vec<String>,
|
||||
#[argp(option, short = 'v')]
|
||||
/// (optional) REL version (default is 3)
|
||||
version: Option<u32>,
|
||||
#[argp(switch, short = 'w')]
|
||||
/// disable warnings
|
||||
no_warn: bool,
|
||||
@ -316,7 +313,7 @@ fn make(args: MakeArgs) -> Result<()> {
|
||||
.unwrap_or(idx as u32);
|
||||
load_obj(file.map()?)
|
||||
.map(|o| LoadedModule { module_id, file: o, path: path.clone() })
|
||||
.with_context(|| format!("Failed to load '{path}'"))
|
||||
.with_context(|| format!("Failed to load '{}'", path))
|
||||
})
|
||||
.collect::<Result<Vec<_>>>()?;
|
||||
|
||||
@ -367,7 +364,7 @@ fn make(args: MakeArgs) -> Result<()> {
|
||||
let _span = info_span!("file", path = %module_info.path).entered();
|
||||
let mut info = RelWriteInfo {
|
||||
module_id: module_info.module_id,
|
||||
version: args.version.unwrap_or(3),
|
||||
version: 3,
|
||||
name_offset: None,
|
||||
name_size: None,
|
||||
align: None,
|
||||
@ -395,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}'"))?;
|
||||
.with_context(|| format!("Failed to write '{}'", rel_path))?;
|
||||
w.flush()?;
|
||||
}
|
||||
|
||||
|
@ -143,7 +143,7 @@ fn make_rso(
|
||||
|
||||
let si = sym
|
||||
.section_index()
|
||||
.with_context(|| format!("Failed to find symbol `{name}` section index"))?;
|
||||
.with_context(|| format!("Failed to find symbol `{}` section index", name))?;
|
||||
let addr = sym.address();
|
||||
|
||||
*index = si.0 as u8;
|
||||
@ -204,7 +204,8 @@ fn make_rso(
|
||||
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.contains(&n));
|
||||
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 {
|
||||
@ -320,7 +321,8 @@ fn make_rso(
|
||||
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.contains(&n));
|
||||
let is_valid_section =
|
||||
section.name().is_ok_and(|n| RSO_SECTION_NAMES.iter().any(|&s| s == n));
|
||||
if !is_valid_section {
|
||||
continue;
|
||||
}
|
||||
|
@ -45,13 +45,14 @@ pub fn run(args: Args) -> Result<()> {
|
||||
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}'"))?;
|
||||
touch(out_path)
|
||||
.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}'"))?,
|
||||
.with_context(|| format!("Failed to open output file '{}'", out_path))?,
|
||||
)
|
||||
} else {
|
||||
Box::new(stdout())
|
||||
|
@ -4,7 +4,6 @@ use anyhow::{anyhow, bail, Context};
|
||||
use argp::FromArgs;
|
||||
use size::Size;
|
||||
use typed_path::{Utf8NativePath, Utf8NativePathBuf, Utf8UnixPath};
|
||||
use unicode_width::UnicodeWidthStr;
|
||||
|
||||
use crate::{
|
||||
util::{file::buf_copy, path::native_path},
|
||||
@ -73,7 +72,7 @@ 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.width_cjk());
|
||||
widths[i] = widths[i].max(column.len());
|
||||
}
|
||||
}
|
||||
widths
|
||||
@ -85,7 +84,7 @@ fn file_info(
|
||||
metadata: &VfsMetadata,
|
||||
) -> anyhow::Result<Columns<5>> {
|
||||
let format =
|
||||
detect(file).with_context(|| format!("Failed to detect file format for {filename}"))?;
|
||||
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(),
|
||||
@ -97,9 +96,9 @@ fn file_info(
|
||||
let mut decompressed = decompress_file(file, kind)?;
|
||||
let metadata = decompressed
|
||||
.metadata()
|
||||
.with_context(|| format!("Failed to fetch metadata for {filename}"))?;
|
||||
.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}"))?;
|
||||
.with_context(|| format!("Failed to detect file format for {}", filename))?;
|
||||
info[3] = format!("Decompressed: {}", Size::from_bytes(metadata.len));
|
||||
info[4] = format.to_string();
|
||||
}
|
||||
@ -112,11 +111,11 @@ pub fn ls(args: LsArgs) -> anyhow::Result<()> {
|
||||
OpenResult::File(mut file, path) => {
|
||||
let filename = path.file_name().ok_or_else(|| anyhow!("Path has no filename"))?;
|
||||
if args.short {
|
||||
println!("{filename}");
|
||||
println!("{}", filename);
|
||||
} else {
|
||||
let metadata = file
|
||||
.metadata()
|
||||
.with_context(|| format!("Failed to fetch metadata for {path}"))?;
|
||||
.with_context(|| format!("Failed to fetch metadata for {}", path))?;
|
||||
files.push(file_info(filename, file.as_mut(), &metadata)?);
|
||||
}
|
||||
}
|
||||
@ -131,14 +130,10 @@ pub fn ls(args: LsArgs) -> anyhow::Result<()> {
|
||||
for (i, column) in entry.iter().enumerate() {
|
||||
if widths[i] > 0 {
|
||||
if written > 0 {
|
||||
print!("{SEPARATOR}");
|
||||
print!("{}", SEPARATOR);
|
||||
}
|
||||
written += 1;
|
||||
print!("{column}");
|
||||
let remain = widths[i].saturating_sub(column.width_cjk());
|
||||
if remain > 0 {
|
||||
print!("{:width$}", "", width = remain);
|
||||
}
|
||||
print!("{:width$}", column, width = widths[i]);
|
||||
}
|
||||
}
|
||||
println!();
|
||||
@ -161,25 +156,25 @@ fn ls_directory(
|
||||
let display_path = base_filename.join(&filename);
|
||||
let metadata = fs
|
||||
.metadata(&entry_path)
|
||||
.with_context(|| format!("Failed to fetch metadata for {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}"))?;
|
||||
.with_context(|| format!("Failed to open file {}", entry_path))?;
|
||||
if args.short {
|
||||
println!("{display_path}");
|
||||
println!("{}", display_path);
|
||||
} else {
|
||||
files.push(file_info(display_path.as_str(), file.as_mut(), &metadata)?);
|
||||
}
|
||||
}
|
||||
VfsFileType::Directory => {
|
||||
if args.short {
|
||||
println!("{display_path}/");
|
||||
println!("{}/", display_path);
|
||||
} else {
|
||||
files.push([
|
||||
" ".to_string(),
|
||||
format!("{display_path}/"),
|
||||
format!("{}/", display_path),
|
||||
"Directory".to_string(),
|
||||
String::new(),
|
||||
String::new(),
|
||||
@ -206,7 +201,7 @@ pub fn cp(mut args: CpArgs) -> anyhow::Result<()> {
|
||||
OpenResult::File(file, path) => {
|
||||
let dest = if dest_is_dir {
|
||||
fs::create_dir_all(&dest)
|
||||
.with_context(|| format!("Failed to create directory {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)
|
||||
@ -234,12 +229,12 @@ fn cp_file(
|
||||
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}"))?;
|
||||
.with_context(|| format!("Failed to decompress file {}", dest))?;
|
||||
compression = Some(kind);
|
||||
}
|
||||
}
|
||||
let metadata =
|
||||
file.metadata().with_context(|| format!("Failed to fetch metadata for {dest}"))?;
|
||||
file.metadata().with_context(|| format!("Failed to fetch metadata for {}", dest))?;
|
||||
if !quiet {
|
||||
if let Some(kind) = compression {
|
||||
println!(
|
||||
@ -254,10 +249,10 @@ fn cp_file(
|
||||
}
|
||||
}
|
||||
let mut dest_file =
|
||||
File::create(dest).with_context(|| format!("Failed to create file {dest}"))?;
|
||||
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}"))?;
|
||||
.with_context(|| format!("Failed to copy file {}", dest))?;
|
||||
dest_file.flush().with_context(|| format!("Failed to flush file {}", dest))?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@ -268,18 +263,18 @@ fn cp_recursive(
|
||||
auto_decompress: bool,
|
||||
quiet: bool,
|
||||
) -> anyhow::Result<()> {
|
||||
fs::create_dir_all(dest).with_context(|| format!("Failed to create directory {dest}"))?;
|
||||
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}"))?;
|
||||
.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}"))?;
|
||||
.with_context(|| format!("Failed to open file {}", entry_path))?;
|
||||
cp_file(file, &entry_path, &dest.join(filename), auto_decompress, quiet)?;
|
||||
}
|
||||
VfsFileType::Directory => {
|
||||
|
108
src/cmd/wad.rs
108
src/cmd/wad.rs
@ -1,108 +0,0 @@
|
||||
use anyhow::Result;
|
||||
use argp::FromArgs;
|
||||
use size::Size;
|
||||
use typed_path::Utf8NativePathBuf;
|
||||
|
||||
use crate::{
|
||||
cmd::vfs,
|
||||
util::{
|
||||
path::native_path,
|
||||
wad::{process_wad, verify_wad},
|
||||
},
|
||||
vfs::open_file,
|
||||
};
|
||||
|
||||
#[derive(FromArgs, PartialEq, Debug)]
|
||||
/// Commands for processing Wii WAD files.
|
||||
#[argp(subcommand, name = "wad")]
|
||||
pub struct Args {
|
||||
#[argp(subcommand)]
|
||||
command: SubCommand,
|
||||
}
|
||||
|
||||
#[derive(FromArgs, PartialEq, Debug)]
|
||||
#[argp(subcommand)]
|
||||
enum SubCommand {
|
||||
Extract(ExtractArgs),
|
||||
Info(InfoArgs),
|
||||
Verify(VerifyArgs),
|
||||
}
|
||||
|
||||
#[derive(FromArgs, PartialEq, Eq, Debug)]
|
||||
/// Extracts WAD file contents.
|
||||
#[argp(subcommand, name = "extract")]
|
||||
pub struct ExtractArgs {
|
||||
#[argp(positional, from_str_fn(native_path))]
|
||||
/// WAD file
|
||||
file: Utf8NativePathBuf,
|
||||
#[argp(option, short = 'o', from_str_fn(native_path))]
|
||||
/// output directory
|
||||
output: Option<Utf8NativePathBuf>,
|
||||
#[argp(switch)]
|
||||
/// Do not decompress files when copying.
|
||||
no_decompress: bool,
|
||||
#[argp(switch, short = 'q')]
|
||||
/// Quiet output. Don't print anything except errors.
|
||||
quiet: bool,
|
||||
}
|
||||
|
||||
#[derive(FromArgs, PartialEq, Eq, Debug)]
|
||||
/// Views WAD file information.
|
||||
#[argp(subcommand, name = "info")]
|
||||
pub struct InfoArgs {
|
||||
#[argp(positional, from_str_fn(native_path))]
|
||||
/// WAD file
|
||||
file: Utf8NativePathBuf,
|
||||
}
|
||||
|
||||
#[derive(FromArgs, PartialEq, Eq, Debug)]
|
||||
/// Verifies WAD file integrity.
|
||||
#[argp(subcommand, name = "verify")]
|
||||
pub struct VerifyArgs {
|
||||
#[argp(positional, from_str_fn(native_path))]
|
||||
/// WAD file
|
||||
file: Utf8NativePathBuf,
|
||||
}
|
||||
|
||||
pub fn run(args: Args) -> Result<()> {
|
||||
match args.command {
|
||||
SubCommand::Info(c_args) => info(c_args),
|
||||
SubCommand::Verify(c_args) => verify(c_args),
|
||||
SubCommand::Extract(c_args) => extract(c_args),
|
||||
}
|
||||
}
|
||||
|
||||
fn info(args: InfoArgs) -> Result<()> {
|
||||
let mut file = open_file(&args.file, true)?;
|
||||
let wad = process_wad(file.as_mut())?;
|
||||
println!("Title ID: {}", hex::encode(wad.ticket().title_id));
|
||||
println!("Title key: {}", hex::encode(wad.title_key));
|
||||
println!("Fake signed: {}", wad.fake_signed);
|
||||
for content in wad.contents() {
|
||||
println!(
|
||||
"Content {:08x}: Offset {:#X}, size {}",
|
||||
content.content_index.get(),
|
||||
wad.content_offset(content.content_index.get()),
|
||||
Size::from_bytes(content.size.get())
|
||||
);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn verify(args: VerifyArgs) -> Result<()> {
|
||||
let mut file = open_file(&args.file, true)?;
|
||||
let wad = process_wad(file.as_mut())?;
|
||||
verify_wad(&wad, file.as_mut())?;
|
||||
println!("Verification successful");
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn extract(args: ExtractArgs) -> Result<()> {
|
||||
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,
|
||||
})
|
||||
}
|
@ -80,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}'"))?;
|
||||
.with_context(|| format!("Failed to write '{}'", out_path))?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
@ -92,7 +92,7 @@ fn decompress(args: DecompressArgs) -> Result<()> {
|
||||
let data = {
|
||||
let mut file = open_file(&path, true)?;
|
||||
decompress_yay0(file.map()?)
|
||||
.with_context(|| format!("Failed to decompress '{path}' using Yay0"))?
|
||||
.with_context(|| format!("Failed to decompress '{}' using Yay0", path))?
|
||||
};
|
||||
let out_path = if let Some(output) = &args.output {
|
||||
if single_file {
|
||||
@ -104,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}'"))?;
|
||||
.with_context(|| format!("Failed to write '{}'", out_path))?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
@ -80,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}'"))?;
|
||||
.with_context(|| format!("Failed to write '{}'", out_path))?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
@ -92,7 +92,7 @@ fn decompress(args: DecompressArgs) -> Result<()> {
|
||||
let data = {
|
||||
let mut file = open_file(&path, false)?;
|
||||
decompress_yaz0(file.map()?)
|
||||
.with_context(|| format!("Failed to decompress '{path}' using Yaz0"))?
|
||||
.with_context(|| format!("Failed to decompress '{}' using Yaz0", path))?
|
||||
};
|
||||
let out_path = if let Some(output) = &args.output {
|
||||
if single_file {
|
||||
@ -104,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}'"))?;
|
||||
.with_context(|| format!("Failed to write '{}'", out_path))?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
@ -96,7 +96,6 @@ enum SubCommand {
|
||||
Dwarf(cmd::dwarf::Args),
|
||||
Elf(cmd::elf::Args),
|
||||
Elf2Dol(cmd::elf2dol::Args),
|
||||
Extab(cmd::extab::Args),
|
||||
Map(cmd::map::Args),
|
||||
Nlzss(cmd::nlzss::Args),
|
||||
Rarc(cmd::rarc::Args),
|
||||
@ -107,7 +106,6 @@ enum SubCommand {
|
||||
Vfs(cmd::vfs::Args),
|
||||
Yay0(cmd::yay0::Args),
|
||||
Yaz0(cmd::yaz0::Args),
|
||||
Wad(cmd::wad::Args),
|
||||
}
|
||||
|
||||
// Duplicated from supports-color so we can check early.
|
||||
@ -173,7 +171,6 @@ fn main() {
|
||||
SubCommand::Dwarf(c_args) => cmd::dwarf::run(c_args),
|
||||
SubCommand::Elf(c_args) => cmd::elf::run(c_args),
|
||||
SubCommand::Elf2Dol(c_args) => cmd::elf2dol::run(c_args),
|
||||
SubCommand::Extab(c_args) => cmd::extab::run(c_args),
|
||||
SubCommand::Map(c_args) => cmd::map::run(c_args),
|
||||
SubCommand::Nlzss(c_args) => cmd::nlzss::run(c_args),
|
||||
SubCommand::Rarc(c_args) => cmd::rarc::run(c_args),
|
||||
@ -184,7 +181,6 @@ fn main() {
|
||||
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),
|
||||
SubCommand::Wad(c_args) => cmd::wad::run(c_args),
|
||||
});
|
||||
if let Err(e) = result {
|
||||
eprintln!("Failed: {e:?}");
|
||||
|
@ -5,7 +5,7 @@ use itertools::Itertools;
|
||||
|
||||
use crate::{
|
||||
obj::{ObjInfo, ObjSection, SectionIndex},
|
||||
util::split::default_section_align,
|
||||
util::{nested::NestedVec, split::default_section_align},
|
||||
};
|
||||
|
||||
/// Marks a split point within a section.
|
||||
@ -107,9 +107,7 @@ impl ObjSplits {
|
||||
}
|
||||
|
||||
pub fn push(&mut self, address: u32, split: ObjSplit) {
|
||||
let out = self.splits.entry(address).or_default();
|
||||
out.push(split);
|
||||
out.sort_by_key(|s| s.end);
|
||||
self.splits.nested_push(address, split);
|
||||
}
|
||||
|
||||
pub fn remove(&mut self, address: u32) -> Option<Vec<ObjSplit>> { self.splits.remove(&address) }
|
||||
|
@ -175,10 +175,8 @@ pub enum ObjDataKind {
|
||||
Float,
|
||||
Double,
|
||||
String,
|
||||
ShiftJIS,
|
||||
String16,
|
||||
StringTable,
|
||||
ShiftJISTable,
|
||||
String16Table,
|
||||
Int,
|
||||
Short,
|
||||
@ -403,7 +401,7 @@ impl ObjSymbols {
|
||||
pub fn iter_ordered(&self) -> impl DoubleEndedIterator<Item = (SymbolIndex, &ObjSymbol)> {
|
||||
self.symbols_by_section
|
||||
.iter()
|
||||
.flat_map(|v| v.values())
|
||||
.flat_map(|v| v.iter().map(|(_, v)| v))
|
||||
.flat_map(move |v| v.iter().map(move |u| (*u, &self.symbols[*u as usize])))
|
||||
}
|
||||
|
||||
@ -450,7 +448,7 @@ impl ObjSymbols {
|
||||
self.symbols_by_section
|
||||
.get(section_idx as usize)
|
||||
.into_iter()
|
||||
.flat_map(|v| v.values())
|
||||
.flat_map(|v| v.iter().map(|(_, v)| v))
|
||||
.flat_map(move |v| v.iter().map(move |u| (*u, &self.symbols[*u as usize])))
|
||||
}
|
||||
|
||||
|
@ -161,7 +161,7 @@ impl FromReader for AlfSymbolKind {
|
||||
match u32::from_reader(reader, e)? {
|
||||
0 => Ok(Self::Function),
|
||||
1 => Ok(Self::Object),
|
||||
v => Err(Error::new(ErrorKind::InvalidData, format!("invalid ALF symbol kind: {v}"))),
|
||||
v => Err(Error::new(ErrorKind::InvalidData, format!("invalid ALF symbol kind: {}", v))),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -442,12 +442,12 @@ where
|
||||
match parse_extab(symbols, entry, section) {
|
||||
Ok(s) => {
|
||||
for line in s.trim_end().lines() {
|
||||
writeln!(w, " * {line}")?;
|
||||
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, " * Failed to decode extab entry: {}", e)?;
|
||||
}
|
||||
}
|
||||
writeln!(w, " */")?;
|
||||
@ -505,7 +505,7 @@ where
|
||||
}
|
||||
current_symbol_kind = find_symbol_kind(current_symbol_kind, symbols, vec)?;
|
||||
current_data_kind = find_data_kind(current_data_kind, symbols, vec)
|
||||
.with_context(|| format!("At address {sym_addr:#010X}"))?;
|
||||
.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 as usize]).collect_vec();
|
||||
@ -660,46 +660,14 @@ where W: Write + ?Sized {
|
||||
'\x0D' => write!(w, "\\r")?,
|
||||
'\\' => write!(w, "\\\\")?,
|
||||
'"' => write!(w, "\\\"")?,
|
||||
c if c.is_ascii_graphic() || c.is_ascii_whitespace() => write!(w, "{c}")?,
|
||||
_ => write!(w, "\\{b:03o}")?,
|
||||
c if c.is_ascii_graphic() || c.is_ascii_whitespace() => write!(w, "{}", c)?,
|
||||
_ => write!(w, "\\{:03o}", b)?,
|
||||
}
|
||||
}
|
||||
writeln!(w, "\"")?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
use encoding_rs::SHIFT_JIS;
|
||||
|
||||
fn write_string_shiftjis<W>(w: &mut W, data: &[u8]) -> Result<()>
|
||||
where W: Write + ?Sized {
|
||||
if data.last() != Some(&0x00) {
|
||||
bail!("Non-terminated Shift-JIS string");
|
||||
}
|
||||
|
||||
let raw_data = &data[..data.len() - 1];
|
||||
|
||||
// Decode then write SJIS as comment above byte array
|
||||
let (cow, _, _) = SHIFT_JIS.decode(raw_data);
|
||||
write!(w, "\t# ")?;
|
||||
for c in cow.chars() {
|
||||
match c {
|
||||
'#' => write!(w, "\\#")?,
|
||||
_ => write!(w, "{c}")?,
|
||||
}
|
||||
}
|
||||
|
||||
write!(w, "\n\t.byte ")?;
|
||||
for (i, &b) in data.iter().enumerate() {
|
||||
write!(w, "0x{b:02X}")?;
|
||||
if i + 1 != data.len() {
|
||||
write!(w, ", ")?;
|
||||
}
|
||||
}
|
||||
|
||||
writeln!(w)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn write_string16<W>(w: &mut W, data: &[u16]) -> Result<()>
|
||||
where W: Write + ?Sized {
|
||||
if matches!(data.last(), Some(&b) if b == 0) {
|
||||
@ -721,7 +689,7 @@ where W: Write + ?Sized {
|
||||
'\x0D' => write!(w, "\\r")?,
|
||||
'\\' => write!(w, "\\\\")?,
|
||||
'"' => write!(w, "\\\"")?,
|
||||
c if c.is_ascii_graphic() || c.is_ascii_whitespace() => write!(w, "{c}")?,
|
||||
c if c.is_ascii_graphic() || c.is_ascii_whitespace() => write!(w, "{}", c)?,
|
||||
_ => write!(w, "\\{:#X}", c as u32)?,
|
||||
}
|
||||
}
|
||||
@ -737,12 +705,6 @@ where W: Write + ?Sized {
|
||||
ObjDataKind::String => {
|
||||
return write_string(w, data);
|
||||
}
|
||||
ObjDataKind::ShiftJIS => {
|
||||
if data.is_empty() || data.last() != Some(&0x00) {
|
||||
anyhow::bail!("Non-terminated Shift-JIS string");
|
||||
}
|
||||
return write_string_shiftjis(w, data);
|
||||
}
|
||||
ObjDataKind::String16 => {
|
||||
if data.len() % 2 != 0 {
|
||||
bail!("Attempted to write wstring with length {:#X}", data.len());
|
||||
@ -772,12 +734,6 @@ where W: Write + ?Sized {
|
||||
}
|
||||
return Ok(());
|
||||
}
|
||||
ObjDataKind::ShiftJISTable => {
|
||||
for slice in data.split_inclusive(|&b| b == 0) {
|
||||
write_string_shiftjis(w, slice)?;
|
||||
}
|
||||
return Ok(());
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
let chunk_size = match data_kind {
|
||||
@ -786,14 +742,12 @@ where W: Write + ?Sized {
|
||||
ObjDataKind::Byte | ObjDataKind::Byte8 | ObjDataKind::Double => 8,
|
||||
ObjDataKind::String
|
||||
| ObjDataKind::String16
|
||||
| ObjDataKind::ShiftJIS
|
||||
| ObjDataKind::StringTable
|
||||
| ObjDataKind::ShiftJISTable
|
||||
| ObjDataKind::String16Table => unreachable!(),
|
||||
};
|
||||
for chunk in remain.chunks(chunk_size) {
|
||||
if data_kind == ObjDataKind::Byte || matches!(chunk.len(), 1 | 3 | 5..=7) {
|
||||
let bytes = chunk.iter().map(|c| format!("{c:#04X}")).collect::<Vec<String>>();
|
||||
let bytes = chunk.iter().map(|c| format!("{:#04X}", c)).collect::<Vec<String>>();
|
||||
writeln!(w, "\t.byte {}", bytes.join(", "))?;
|
||||
} else {
|
||||
match chunk.len() {
|
||||
|
@ -50,26 +50,15 @@ impl fmt::Display for HeaderKind {
|
||||
}
|
||||
|
||||
/// Converts a binary blob into a C array.
|
||||
pub fn bin2c(
|
||||
symbol: &ObjSymbol,
|
||||
section: &ObjSection,
|
||||
data: &[u8],
|
||||
kind: HeaderKind,
|
||||
rename: Option<&str>,
|
||||
) -> String {
|
||||
pub fn bin2c(symbol: &ObjSymbol, section: &ObjSection, data: &[u8], kind: HeaderKind) -> String {
|
||||
match kind {
|
||||
HeaderKind::None => String::new(),
|
||||
HeaderKind::Symbol => bin2c_symbol(symbol, section, data, rename),
|
||||
HeaderKind::Symbol => bin2c_symbol(symbol, section, data),
|
||||
HeaderKind::Raw => bin2c_raw(data),
|
||||
}
|
||||
}
|
||||
|
||||
fn bin2c_symbol(
|
||||
symbol: &ObjSymbol,
|
||||
section: &ObjSection,
|
||||
data: &[u8],
|
||||
rename: Option<&str>,
|
||||
) -> String {
|
||||
fn bin2c_symbol(symbol: &ObjSymbol, section: &ObjSection, data: &[u8]) -> String {
|
||||
let mut output = String::new();
|
||||
output.push_str(PROLOGUE);
|
||||
output.push_str(&format!(
|
||||
@ -83,11 +72,7 @@ fn bin2c_symbol(
|
||||
output.push_str("const ");
|
||||
}
|
||||
output.push_str("unsigned char ");
|
||||
if let Some(rename) = rename {
|
||||
output.push_str(rename);
|
||||
} else {
|
||||
output.push_str(symbol.demangled_name.as_deref().unwrap_or(symbol.name.as_str()));
|
||||
}
|
||||
output.push_str(symbol.demangled_name.as_deref().unwrap_or(symbol.name.as_str()));
|
||||
output.push_str(&format!("[] ATTRIBUTE_ALIGN({}) = {{", symbol.align.unwrap_or(4)));
|
||||
for (i, byte) in data.iter().enumerate() {
|
||||
if i % 16 == 0 {
|
||||
@ -95,7 +80,7 @@ fn bin2c_symbol(
|
||||
} else {
|
||||
output.push(' ');
|
||||
}
|
||||
output.push_str(&format!("0x{byte:02X},"));
|
||||
output.push_str(&format!("0x{:02X},", byte));
|
||||
}
|
||||
output.push_str("\n};\n");
|
||||
output
|
||||
@ -111,7 +96,7 @@ fn bin2c_raw(data: &[u8]) -> String {
|
||||
output.push(' ');
|
||||
}
|
||||
}
|
||||
output.push_str(&format!("0x{byte:02X},"));
|
||||
output.push_str(&format!("0x{:02X},", byte));
|
||||
}
|
||||
output.push('\n');
|
||||
output
|
||||
|
@ -58,7 +58,7 @@ impl FromReader for MWComment {
|
||||
if magic != MAGIC {
|
||||
return Err(io::Error::new(
|
||||
io::ErrorKind::InvalidData,
|
||||
format!("Invalid .comment section magic: {magic:?}"),
|
||||
format!("Invalid .comment section magic: {:?}", magic),
|
||||
));
|
||||
}
|
||||
// 0xB
|
||||
@ -78,7 +78,7 @@ impl FromReader for MWComment {
|
||||
value => {
|
||||
return Err(io::Error::new(
|
||||
io::ErrorKind::InvalidData,
|
||||
format!("Invalid value for pool_data: {value}"),
|
||||
format!("Invalid value for pool_data: {}", value),
|
||||
))
|
||||
}
|
||||
};
|
||||
@ -93,7 +93,7 @@ impl FromReader for MWComment {
|
||||
v => {
|
||||
return Err(io::Error::new(
|
||||
io::ErrorKind::InvalidData,
|
||||
format!("Expected header size {HEADER_SIZE:#X}, got {v:#X}"),
|
||||
format!("Expected header size {:#X}, got {:#X}", HEADER_SIZE, v),
|
||||
))
|
||||
}
|
||||
}
|
||||
@ -102,7 +102,7 @@ impl FromReader for MWComment {
|
||||
if flags & !7 != 0 {
|
||||
return Err(io::Error::new(
|
||||
io::ErrorKind::InvalidData,
|
||||
format!("Unexpected flag value {flags:#X}"),
|
||||
format!("Unexpected flag value {:#X}", flags),
|
||||
));
|
||||
}
|
||||
if flags & 1 == 1 {
|
||||
@ -221,14 +221,14 @@ impl FromReader for CommentSym {
|
||||
if value != 0 {
|
||||
return Err(io::Error::new(
|
||||
io::ErrorKind::InvalidData,
|
||||
format!("Unexpected value after active_flags (1): {value:#X}"),
|
||||
format!("Unexpected value after active_flags (1): {:#X}", value),
|
||||
));
|
||||
}
|
||||
let value = u8::from_reader(reader, e)?;
|
||||
if value != 0 {
|
||||
return Err(io::Error::new(
|
||||
io::ErrorKind::InvalidData,
|
||||
format!("Unexpected value after active_flags (2): {value:#X}"),
|
||||
format!("Unexpected value after active_flags (2): {:#X}", value),
|
||||
));
|
||||
}
|
||||
Ok(out)
|
||||
|
@ -282,11 +282,11 @@ where W: Write + ?Sized {
|
||||
write!(w, " data:{kind}")?;
|
||||
}
|
||||
if let Some(hash) = symbol.name_hash {
|
||||
write!(w, " hash:{hash:#010X}")?;
|
||||
write!(w, " hash:{:#010X}", hash)?;
|
||||
}
|
||||
if let Some(hash) = symbol.demangled_name_hash {
|
||||
if symbol.name_hash != symbol.demangled_name_hash {
|
||||
write!(w, " dhash:{hash:#010X}")?;
|
||||
write!(w, " dhash:{:#010X}", hash)?;
|
||||
}
|
||||
}
|
||||
if symbol.flags.is_hidden() {
|
||||
@ -329,10 +329,8 @@ fn symbol_data_kind_to_str(kind: ObjDataKind) -> Option<&'static str> {
|
||||
ObjDataKind::Float => Some("float"),
|
||||
ObjDataKind::Double => Some("double"),
|
||||
ObjDataKind::String => Some("string"),
|
||||
ObjDataKind::ShiftJIS => Some("sjis"),
|
||||
ObjDataKind::String16 => Some("wstring"),
|
||||
ObjDataKind::StringTable => Some("string_table"),
|
||||
ObjDataKind::ShiftJISTable => Some("sjis_table"),
|
||||
ObjDataKind::String16Table => Some("wstring_table"),
|
||||
ObjDataKind::Int => Some("int"),
|
||||
ObjDataKind::Short => Some("short"),
|
||||
@ -384,10 +382,8 @@ fn symbol_data_kind_from_str(s: &str) -> Option<ObjDataKind> {
|
||||
"float" => Some(ObjDataKind::Float),
|
||||
"double" => Some(ObjDataKind::Double),
|
||||
"string" => Some(ObjDataKind::String),
|
||||
"sjis" => Some(ObjDataKind::ShiftJIS),
|
||||
"wstring" => Some(ObjDataKind::String16),
|
||||
"string_table" => Some(ObjDataKind::StringTable),
|
||||
"sjis_table" => Some(ObjDataKind::ShiftJISTable),
|
||||
"wstring_table" => Some(ObjDataKind::String16Table),
|
||||
"int" => Some(ObjDataKind::Int),
|
||||
"short" => Some(ObjDataKind::Short),
|
||||
@ -439,10 +435,10 @@ where W: Write + ?Sized {
|
||||
for unit in obj.link_order.iter().filter(|unit| all || !unit.autogenerated) {
|
||||
write!(w, "\n{}:", unit.name)?;
|
||||
if let Some(comment_version) = unit.comment_version {
|
||||
write!(w, " comment:{comment_version}")?;
|
||||
write!(w, " comment:{}", comment_version)?;
|
||||
}
|
||||
if let Some(order) = unit.order {
|
||||
write!(w, " order:{order}")?;
|
||||
write!(w, " order:{}", order)?;
|
||||
}
|
||||
writeln!(w)?;
|
||||
let mut split_iter = obj.sections.all_splits().peekable();
|
||||
@ -458,14 +454,14 @@ where W: Write + ?Sized {
|
||||
write!(w, "\t{:<11} start:{:#010X} end:{:#010X}", section.name, addr, end)?;
|
||||
if let Some(align) = split.align {
|
||||
if align != default_section_align(section) as u32 {
|
||||
write!(w, " align:{align}")?;
|
||||
write!(w, " align:{}", align)?;
|
||||
}
|
||||
}
|
||||
if split.common {
|
||||
write!(w, " common")?;
|
||||
}
|
||||
if let Some(name) = &split.rename {
|
||||
write!(w, " rename:{name}")?;
|
||||
write!(w, " rename:{}", name)?;
|
||||
}
|
||||
if split.skip {
|
||||
write!(w, " skip")?;
|
||||
@ -714,7 +710,7 @@ where R: BufRead + ?Sized {
|
||||
ensure!(
|
||||
section.contains_range(start..end)
|
||||
|| (start == section_end && end == section_end),
|
||||
"Section {} ({:#010X}..{:#010X}) does not contain range {:#010X}..{:#010X}. Check splits.txt?",
|
||||
"Section {} ({:#010X}..{:#010X}) does not contain range {:#010X}..{:#010X}",
|
||||
name,
|
||||
section.address,
|
||||
section.address + section.size,
|
||||
@ -783,7 +779,7 @@ pub mod signed_hex_serde {
|
||||
if *value < 0 {
|
||||
serializer.serialize_str(&format!("-{:#X}", -value))
|
||||
} else {
|
||||
serializer.serialize_str(&format!("{value:#X}"))
|
||||
serializer.serialize_str(&format!("{:#X}", value))
|
||||
}
|
||||
}
|
||||
|
||||
@ -791,7 +787,7 @@ pub mod signed_hex_serde {
|
||||
where D: Deserializer<'de> {
|
||||
struct SignedHexVisitor;
|
||||
|
||||
impl serde::de::Visitor<'_> for SignedHexVisitor {
|
||||
impl<'de> serde::de::Visitor<'de> for SignedHexVisitor {
|
||||
type Value = i64;
|
||||
|
||||
fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result {
|
||||
@ -864,7 +860,7 @@ impl<'de> serde::Deserialize<'de> for SectionAddressRef {
|
||||
where D: serde::Deserializer<'de> {
|
||||
struct SectionAddressRefVisitor;
|
||||
|
||||
impl serde::de::Visitor<'_> for SectionAddressRefVisitor {
|
||||
impl<'de> serde::de::Visitor<'de> for SectionAddressRefVisitor {
|
||||
type Value = SectionAddressRef;
|
||||
|
||||
fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result {
|
||||
|
@ -209,7 +209,7 @@ fn print_line(ins_diff: &ObjInsDiff, base_addr: u64) -> Vec<Span> {
|
||||
pad_to = 5;
|
||||
}
|
||||
DiffText::Address(addr) => {
|
||||
label_text = format!("{addr:x}:");
|
||||
label_text = format!("{:x}:", addr);
|
||||
pad_to = 5;
|
||||
}
|
||||
DiffText::Opcode(mnemonic, _op) => {
|
||||
|
833
src/util/dol.rs
833
src/util/dol.rs
File diff suppressed because it is too large
Load Diff
@ -358,7 +358,6 @@ pub struct Tag {
|
||||
pub kind: TagKind,
|
||||
pub is_erased: bool, // Tag was deleted but has been reconstructed
|
||||
pub is_erased_root: bool, // Tag is erased and is the root of a tree of erased tags
|
||||
pub data_endian: Endian, // Endianness of the tag data (could be different from the address endianness for erased tags)
|
||||
pub attributes: Vec<Attribute>,
|
||||
}
|
||||
|
||||
@ -555,7 +554,6 @@ where
|
||||
kind: TagKind::Padding,
|
||||
is_erased,
|
||||
is_erased_root: false,
|
||||
data_endian,
|
||||
attributes: Vec::new(),
|
||||
});
|
||||
return Ok(tags);
|
||||
@ -565,42 +563,26 @@ where
|
||||
let tag = TagKind::try_from(tag_num).context("Unknown DWARF tag type")?;
|
||||
if tag == TagKind::Padding {
|
||||
if include_erased {
|
||||
// Erased entries that have become padding could be either
|
||||
// little-endian or big-endian, and we have to guess the length and
|
||||
// tag of the first entry. We assume the entry is either a variable
|
||||
// or a function, and read until we find the high_pc attribute. Only
|
||||
// MwGlobalRef will follow, and these are unlikely to be confused
|
||||
// with the length of the next entry.
|
||||
// Erased entries that have become padding are little-endian, and we
|
||||
// have to guess the length and tag of the first entry. We assume
|
||||
// the entry is either a variable or a function, and read until we
|
||||
// find the high_pc attribute. Only MwGlobalRef will follow, and
|
||||
// these are unlikely to be confused with the length of the next
|
||||
// entry.
|
||||
let mut attributes = Vec::new();
|
||||
let mut is_function = false;
|
||||
|
||||
// Guess endianness based on first attribute
|
||||
let data_endian = if is_erased {
|
||||
data_endian
|
||||
} else {
|
||||
// Peek next two bytes
|
||||
let mut buf = [0u8; 2];
|
||||
reader.read_exact(&mut buf)?;
|
||||
let attr_tag = u16::from_reader(&mut Cursor::new(&buf), data_endian)?;
|
||||
reader.seek(SeekFrom::Current(-2))?;
|
||||
match AttributeKind::try_from(attr_tag) {
|
||||
Ok(_) => data_endian,
|
||||
Err(_) => data_endian.flip(),
|
||||
}
|
||||
};
|
||||
|
||||
while reader.stream_position()? < position + size as u64 {
|
||||
// Peek next two bytes
|
||||
let mut buf = [0u8; 2];
|
||||
reader.read_exact(&mut buf)?;
|
||||
let attr_tag = u16::from_reader(&mut Cursor::new(&buf), data_endian)?;
|
||||
let attr_tag = u16::from_reader(&mut Cursor::new(&buf), Endian::Little)?;
|
||||
reader.seek(SeekFrom::Current(-2))?;
|
||||
|
||||
if is_function && attr_tag != AttributeKind::MwGlobalRef as u16 {
|
||||
break;
|
||||
}
|
||||
|
||||
let attr = read_attribute(reader, data_endian, addr_endian)?;
|
||||
let attr = read_attribute(reader, Endian::Little, addr_endian)?;
|
||||
if attr.kind == AttributeKind::HighPc {
|
||||
is_function = true;
|
||||
}
|
||||
@ -612,13 +594,12 @@ where
|
||||
kind,
|
||||
is_erased: true,
|
||||
is_erased_root: true,
|
||||
data_endian,
|
||||
attributes,
|
||||
});
|
||||
|
||||
// Read the rest of the tags
|
||||
while reader.stream_position()? < position + size as u64 {
|
||||
for tag in read_tags(reader, data_endian, addr_endian, include_erased, true)? {
|
||||
for tag in read_tags(reader, Endian::Little, addr_endian, include_erased, true)? {
|
||||
tags.push(tag);
|
||||
}
|
||||
}
|
||||
@ -635,7 +616,6 @@ where
|
||||
kind: tag,
|
||||
is_erased,
|
||||
is_erased_root: false,
|
||||
data_endian,
|
||||
attributes,
|
||||
});
|
||||
}
|
||||
@ -1165,8 +1145,8 @@ fn structure_type_string(
|
||||
struct_def_string(info, typedefs, t)?
|
||||
} else if include_keyword {
|
||||
match t.kind {
|
||||
StructureKind::Struct => format!("struct {name}"),
|
||||
StructureKind::Class => format!("class {name}"),
|
||||
StructureKind::Struct => format!("struct {}", name),
|
||||
StructureKind::Class => format!("class {}", name),
|
||||
}
|
||||
} else {
|
||||
name.clone()
|
||||
@ -1198,7 +1178,7 @@ fn enumeration_type_string(
|
||||
if name.starts_with('@') {
|
||||
enum_def_string(t)?
|
||||
} else if include_keyword {
|
||||
format!("enum {name}")
|
||||
format!("enum {}", name)
|
||||
} else {
|
||||
name.clone()
|
||||
}
|
||||
@ -1223,7 +1203,7 @@ fn union_type_string(
|
||||
if name.starts_with('@') {
|
||||
union_def_string(info, typedefs, t)?
|
||||
} else if include_keyword {
|
||||
format!("union {name}")
|
||||
format!("union {}", name)
|
||||
} else {
|
||||
name.clone()
|
||||
}
|
||||
@ -1326,7 +1306,7 @@ pub fn subroutine_type_string(
|
||||
write!(parameters, "{}{}", ts.prefix, ts.suffix)?;
|
||||
}
|
||||
if let Some(location) = ¶meter.location {
|
||||
write!(parameters, " /* {location} */")?;
|
||||
write!(parameters, " /* {} */", location)?;
|
||||
}
|
||||
}
|
||||
if t.var_args {
|
||||
@ -1342,7 +1322,7 @@ pub fn subroutine_type_string(
|
||||
let base_name = tag
|
||||
.string_attribute(AttributeKind::Name)
|
||||
.ok_or_else(|| anyhow!("member_of tag {} has no name attribute", member_of))?;
|
||||
out.member = format!("{base_name}::");
|
||||
out.member = format!("{}::", base_name);
|
||||
}
|
||||
Ok(out)
|
||||
}
|
||||
@ -1357,7 +1337,7 @@ pub fn subroutine_def_string(
|
||||
if is_erased {
|
||||
out.push_str("// Erased\n");
|
||||
} else if let (Some(start), Some(end)) = (t.start_address, t.end_address) {
|
||||
writeln!(out, "// Range: {start:#X} -> {end:#X}")?;
|
||||
writeln!(out, "// Range: {:#X} -> {:#X}", start, end)?;
|
||||
}
|
||||
let rt = type_string(info, typedefs, &t.return_type, true)?;
|
||||
if t.local {
|
||||
@ -1381,15 +1361,15 @@ pub fn subroutine_def_string(
|
||||
let base_name = tag
|
||||
.string_attribute(AttributeKind::Name)
|
||||
.ok_or_else(|| anyhow!("member_of tag {} has no name attribute", member_of))?;
|
||||
write!(out, "{base_name}::")?;
|
||||
write!(out, "{}::", base_name)?;
|
||||
|
||||
// Handle constructors and destructors
|
||||
if let Some(name) = t.name.as_ref() {
|
||||
if name == "__dt" {
|
||||
write!(out, "~{base_name}")?;
|
||||
write!(out, "~{}", base_name)?;
|
||||
name_written = true;
|
||||
} else if name == "__ct" {
|
||||
write!(out, "{base_name}")?;
|
||||
write!(out, "{}", base_name)?;
|
||||
name_written = true;
|
||||
}
|
||||
}
|
||||
@ -1418,7 +1398,7 @@ pub fn subroutine_def_string(
|
||||
write!(parameters, "{}{}", ts.prefix, ts.suffix)?;
|
||||
}
|
||||
if let Some(location) = ¶meter.location {
|
||||
write!(parameters, " /* {location} */")?;
|
||||
write!(parameters, " /* {} */", location)?;
|
||||
}
|
||||
}
|
||||
if t.var_args {
|
||||
@ -1440,7 +1420,7 @@ pub fn subroutine_def_string(
|
||||
ts.suffix
|
||||
)?;
|
||||
if let Some(location) = &variable.location {
|
||||
write!(var_out, " // {location}")?;
|
||||
write!(var_out, " // {}", location)?;
|
||||
}
|
||||
writeln!(var_out)?;
|
||||
}
|
||||
@ -1455,7 +1435,7 @@ pub fn subroutine_def_string(
|
||||
.get(&reference)
|
||||
.ok_or_else(|| anyhow!("Failed to locate reference tag {}", reference))?;
|
||||
if tag.kind == TagKind::Padding {
|
||||
writeln!(out, " // -> ??? ({reference})")?;
|
||||
writeln!(out, " // -> ??? ({})", reference)?;
|
||||
continue;
|
||||
}
|
||||
let variable = process_variable_tag(info, tag)?;
|
||||
@ -1497,13 +1477,13 @@ fn subroutine_block_string(
|
||||
) -> Result<String> {
|
||||
let mut out = String::new();
|
||||
if let Some(name) = &block.name {
|
||||
write!(out, "{name}: ")?;
|
||||
write!(out, "{}: ", name)?;
|
||||
} else {
|
||||
out.push_str("/* anonymous block */ ");
|
||||
}
|
||||
out.push_str("{\n");
|
||||
if let (Some(start), Some(end)) = (block.start_address, block.end_address) {
|
||||
writeln!(out, " // Range: {start:#X} -> {end:#X}")?;
|
||||
writeln!(out, " // Range: {:#X} -> {:#X}", start, end)?;
|
||||
}
|
||||
let mut var_out = String::new();
|
||||
for variable in &block.variables {
|
||||
@ -1516,7 +1496,7 @@ fn subroutine_block_string(
|
||||
ts.suffix
|
||||
)?;
|
||||
if let Some(location) = &variable.location {
|
||||
write!(var_out, " // {location}")?;
|
||||
write!(var_out, " // {}", location)?;
|
||||
}
|
||||
writeln!(var_out)?;
|
||||
}
|
||||
@ -1655,9 +1635,9 @@ pub fn struct_def_string(
|
||||
};
|
||||
if let Some(name) = t.name.as_ref() {
|
||||
if name.starts_with('@') {
|
||||
write!(out, " /* {name} */")?;
|
||||
write!(out, " /* {} */", name)?;
|
||||
} else {
|
||||
write!(out, " {name}")?;
|
||||
write!(out, " {}", name)?;
|
||||
}
|
||||
}
|
||||
let mut wrote_base = false;
|
||||
@ -1685,7 +1665,7 @@ pub fn struct_def_string(
|
||||
}
|
||||
out.push_str(" {\n");
|
||||
if let Some(byte_size) = t.byte_size {
|
||||
writeln!(out, " // total size: {byte_size:#X}")?;
|
||||
writeln!(out, " // total size: {:#X}", byte_size)?;
|
||||
}
|
||||
let mut vis = match t.kind {
|
||||
StructureKind::Struct => Visibility::Public,
|
||||
@ -1771,9 +1751,9 @@ pub fn enum_def_string(t: &EnumerationType) -> Result<String> {
|
||||
let mut out = match t.name.as_ref() {
|
||||
Some(name) => {
|
||||
if name.starts_with('@') {
|
||||
format!("enum /* {name} */ {{\n")
|
||||
format!("enum /* {} */ {{\n", name)
|
||||
} else {
|
||||
format!("enum {name} {{\n")
|
||||
format!("enum {} {{\n", name)
|
||||
}
|
||||
}
|
||||
None => "enum {\n".to_string(),
|
||||
@ -1789,9 +1769,9 @@ pub fn union_def_string(info: &DwarfInfo, typedefs: &TypedefMap, t: &UnionType)
|
||||
let mut out = match t.name.as_ref() {
|
||||
Some(name) => {
|
||||
if name.starts_with('@') {
|
||||
format!("union /* {name} */ {{\n")
|
||||
format!("union /* {} */ {{\n", name)
|
||||
} else {
|
||||
format!("union {name} {{\n")
|
||||
format!("union {} {{\n", name)
|
||||
}
|
||||
}
|
||||
None => "union {\n".to_string(),
|
||||
@ -2048,9 +2028,9 @@ fn process_array_tag(info: &DwarfInfo, tag: &Tag) -> Result<ArrayType> {
|
||||
(AttributeKind::Sibling, _) => {}
|
||||
(AttributeKind::SubscrData, AttributeValue::Block(data)) => {
|
||||
subscr_data =
|
||||
Some(process_array_subscript_data(data, info.e).with_context(|| {
|
||||
format!("Failed to process SubscrData for tag: {tag:?}")
|
||||
})?)
|
||||
Some(process_array_subscript_data(data, info.e, tag.is_erased).with_context(
|
||||
|| format!("Failed to process SubscrData for tag: {:?}", tag),
|
||||
)?)
|
||||
}
|
||||
(AttributeKind::Ordering, val) => match val {
|
||||
AttributeValue::Data2(d2) => {
|
||||
@ -2076,7 +2056,11 @@ fn process_array_tag(info: &DwarfInfo, tag: &Tag) -> Result<ArrayType> {
|
||||
Ok(ArrayType { element_type: Box::from(element_type), dimensions })
|
||||
}
|
||||
|
||||
fn process_array_subscript_data(data: &[u8], e: Endian) -> Result<(Type, Vec<ArrayDimension>)> {
|
||||
fn process_array_subscript_data(
|
||||
data: &[u8],
|
||||
e: Endian,
|
||||
is_erased: bool,
|
||||
) -> Result<(Type, Vec<ArrayDimension>)> {
|
||||
let mut element_type = None;
|
||||
let mut dimensions = Vec::new();
|
||||
let mut data = data;
|
||||
@ -2117,7 +2101,8 @@ fn process_array_subscript_data(data: &[u8], e: Endian) -> Result<(Type, Vec<Arr
|
||||
SubscriptFormat::ElementType => {
|
||||
let mut cursor = Cursor::new(data);
|
||||
// TODO: is this the right endianness to use for erased tags?
|
||||
let type_attr = read_attribute(&mut cursor, e, e)?;
|
||||
let type_attr =
|
||||
read_attribute(&mut cursor, if is_erased { Endian::Little } else { e }, e)?;
|
||||
element_type = Some(process_type(&type_attr, e)?);
|
||||
data = &data[cursor.position() as usize..];
|
||||
}
|
||||
@ -2471,7 +2456,10 @@ fn process_subroutine_parameter_tag(info: &DwarfInfo, tag: &Tag) -> Result<Subro
|
||||
) => kind = Some(process_type(attr, info.e)?),
|
||||
(AttributeKind::Location, AttributeValue::Block(block)) => {
|
||||
if !block.is_empty() {
|
||||
location = Some(process_variable_location(block, tag.data_endian)?);
|
||||
location = Some(process_variable_location(
|
||||
block,
|
||||
if tag.is_erased { Endian::Little } else { info.e },
|
||||
)?);
|
||||
}
|
||||
}
|
||||
(AttributeKind::MwDwarf2Location, AttributeValue::Block(_block)) => {
|
||||
@ -2526,7 +2514,10 @@ fn process_local_variable_tag(info: &DwarfInfo, tag: &Tag) -> Result<SubroutineV
|
||||
) => kind = Some(process_type(attr, info.e)?),
|
||||
(AttributeKind::Location, AttributeValue::Block(block)) => {
|
||||
if !block.is_empty() {
|
||||
location = Some(process_variable_location(block, tag.data_endian)?);
|
||||
location = Some(process_variable_location(
|
||||
block,
|
||||
if tag.is_erased { Endian::Little } else { info.e },
|
||||
)?);
|
||||
}
|
||||
}
|
||||
(AttributeKind::MwDwarf2Location, AttributeValue::Block(_block)) => {
|
||||
@ -2624,13 +2615,13 @@ pub fn process_type(attr: &Attribute, e: Endian) -> Result<Type> {
|
||||
match (attr.kind, &attr.value) {
|
||||
(AttributeKind::FundType, &AttributeValue::Data2(type_id)) => {
|
||||
let fund_type = FundType::parse_int(type_id)
|
||||
.with_context(|| format!("Invalid fundamental type ID '{type_id:04X}'"))?;
|
||||
.with_context(|| format!("Invalid fundamental type ID '{:04X}'", type_id))?;
|
||||
Ok(Type { kind: TypeKind::Fundamental(fund_type), modifiers: vec![] })
|
||||
}
|
||||
(AttributeKind::ModFundType, AttributeValue::Block(ops)) => {
|
||||
let type_id = u16::from_bytes(ops[ops.len() - 2..].try_into()?, e);
|
||||
let fund_type = FundType::parse_int(type_id)
|
||||
.with_context(|| format!("Invalid fundamental type ID '{type_id:04X}'"))?;
|
||||
.with_context(|| format!("Invalid fundamental type ID '{:04X}'", type_id))?;
|
||||
let modifiers = process_modifiers(&ops[..ops.len() - 2])?;
|
||||
Ok(Type { kind: TypeKind::Fundamental(fund_type), modifiers })
|
||||
}
|
||||
@ -2771,7 +2762,7 @@ pub fn tag_type_string(
|
||||
match ud {
|
||||
UserDefinedType::Structure(_)
|
||||
| UserDefinedType::Enumeration(_)
|
||||
| UserDefinedType::Union(_) => Ok(format!("{ud_str};")),
|
||||
| UserDefinedType::Union(_) => Ok(format!("{};", ud_str)),
|
||||
_ => Ok(ud_str),
|
||||
}
|
||||
}
|
||||
@ -2798,9 +2789,9 @@ fn variable_string(
|
||||
out.push(';');
|
||||
if include_extra {
|
||||
let size = variable.kind.size(info)?;
|
||||
out.push_str(&format!(" // size: {size:#X}"));
|
||||
out.push_str(&format!(" // size: {:#X}", size));
|
||||
if let Some(addr) = variable.address {
|
||||
out.push_str(&format!(", address: {addr:#X}"));
|
||||
out.push_str(&format!(", address: {:#X}", addr));
|
||||
}
|
||||
}
|
||||
Ok(out)
|
||||
|
109
src/util/elf.rs
109
src/util/elf.rs
@ -17,10 +17,10 @@ use object::{
|
||||
elf::{ProgramHeader, Rel, SectionHeader, SectionIndex, SymbolIndex, Writer},
|
||||
StringId,
|
||||
},
|
||||
Architecture, Endianness, File, Object, ObjectKind, ObjectSection, ObjectSymbol, Relocation,
|
||||
Architecture, Endianness, Object, ObjectKind, ObjectSection, ObjectSymbol, Relocation,
|
||||
RelocationFlags, RelocationTarget, SectionKind, Symbol, SymbolKind, SymbolScope, SymbolSection,
|
||||
};
|
||||
use typed_path::{Utf8NativePath, Utf8NativePathBuf};
|
||||
use typed_path::Utf8NativePath;
|
||||
|
||||
use crate::{
|
||||
array_ref,
|
||||
@ -49,7 +49,7 @@ enum BoundaryState {
|
||||
|
||||
pub fn process_elf(path: &Utf8NativePath) -> Result<ObjInfo> {
|
||||
let mut file = open_file(path, true)?;
|
||||
let obj_file = File::parse(file.map()?)?;
|
||||
let obj_file = object::read::File::parse(file.map()?)?;
|
||||
let architecture = match obj_file.architecture() {
|
||||
Architecture::PowerPc => ObjArchitecture::PowerPc,
|
||||
arch => bail!("Unexpected architecture: {arch:?}"),
|
||||
@ -106,14 +106,49 @@ pub fn process_elf(path: &Utf8NativePath) -> Result<ObjInfo> {
|
||||
});
|
||||
}
|
||||
|
||||
let mw_comment = load_comment(&obj_file).unwrap_or_else(|e| {
|
||||
log::warn!("Failed to read .comment section: {e:#}");
|
||||
let mw_comment = if let Some(comment_section) = obj_file.section_by_name(".comment") {
|
||||
let data = comment_section.uncompressed_data()?;
|
||||
if data.is_empty() {
|
||||
None
|
||||
} else {
|
||||
let mut reader = Cursor::new(&*data);
|
||||
let header = MWComment::from_reader(&mut reader, Endian::Big)
|
||||
.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);
|
||||
comment_syms.push(comment_sym);
|
||||
}
|
||||
ensure!(
|
||||
data.len() - reader.position() as usize == 0,
|
||||
".comment section data not fully read"
|
||||
);
|
||||
Some((header, comment_syms))
|
||||
}
|
||||
} else {
|
||||
None
|
||||
});
|
||||
let split_meta = load_split_meta(&obj_file).unwrap_or_else(|e| {
|
||||
log::warn!("Failed to read .note.split section: {e:#}");
|
||||
};
|
||||
|
||||
let split_meta = if let Some(split_meta_section) = obj_file.section_by_name(SPLITMETA_SECTION) {
|
||||
let data = split_meta_section.uncompressed_data()?;
|
||||
if data.is_empty() {
|
||||
None
|
||||
} else {
|
||||
let metadata = SplitMeta::from_section(
|
||||
split_meta_section,
|
||||
obj_file.endianness(),
|
||||
obj_file.is_64(),
|
||||
)
|
||||
.context("While reading .note.split section")?;
|
||||
log::debug!("Loaded .note.split section");
|
||||
Some(metadata)
|
||||
}
|
||||
} else {
|
||||
None
|
||||
});
|
||||
};
|
||||
|
||||
let mut symbols: Vec<ObjSymbol> = vec![];
|
||||
let mut symbol_indexes: Vec<Option<ObjSymbolIndex>> = vec![None /* ELF null symbol */];
|
||||
@ -164,7 +199,7 @@ pub fn process_elf(path: &Utf8NativePath) -> Result<ObjInfo> {
|
||||
hash_map::Entry::Vacant(e) => e.insert(0),
|
||||
};
|
||||
*index += 1;
|
||||
let new_name = format!("{file_name}_{index}");
|
||||
let new_name = format!("{}_{}", file_name, index);
|
||||
// log::info!("Renaming {} to {}", file_name, new_name);
|
||||
file_name.clone_from(&new_name);
|
||||
match section_starts.entry(new_name.clone()) {
|
||||
@ -275,8 +310,8 @@ pub fn process_elf(path: &Utf8NativePath) -> Result<ObjInfo> {
|
||||
continue;
|
||||
}
|
||||
symbol_indexes.push(Some(symbols.len() as ObjSymbolIndex));
|
||||
let comment_sym = mw_comment.as_ref().map(|(_, vec)| &vec[symbol.index().0 - 1]);
|
||||
symbols.push(to_obj_symbol(&obj_file, &symbol, §ion_indexes, comment_sym)?);
|
||||
let align = mw_comment.as_ref().map(|(_, vec)| vec[symbol.index().0].align);
|
||||
symbols.push(to_obj_symbol(&obj_file, &symbol, §ion_indexes, align)?);
|
||||
}
|
||||
|
||||
let mut link_order = Vec::<ObjUnit>::new();
|
||||
@ -349,42 +384,6 @@ pub fn process_elf(path: &Utf8NativePath) -> Result<ObjInfo> {
|
||||
Ok(obj)
|
||||
}
|
||||
|
||||
fn load_split_meta(obj_file: &File) -> Result<Option<SplitMeta>> {
|
||||
let Some(split_meta_section) = obj_file.section_by_name(SPLITMETA_SECTION) else {
|
||||
return Ok(None);
|
||||
};
|
||||
let data = split_meta_section.uncompressed_data()?;
|
||||
if data.is_empty() {
|
||||
return Ok(None);
|
||||
}
|
||||
let metadata =
|
||||
SplitMeta::from_section(split_meta_section, obj_file.endianness(), obj_file.is_64())?;
|
||||
log::debug!("Loaded .note.split section");
|
||||
Ok(Some(metadata))
|
||||
}
|
||||
|
||||
fn load_comment(obj_file: &File) -> Result<Option<(MWComment, Vec<CommentSym>)>> {
|
||||
let Some(comment_section) = obj_file.section_by_name(".comment") else {
|
||||
return Ok(None);
|
||||
};
|
||||
let data = comment_section.uncompressed_data()?;
|
||||
if data.is_empty() {
|
||||
return Ok(None);
|
||||
}
|
||||
let mut reader = Cursor::new(&*data);
|
||||
let header = MWComment::from_reader(&mut reader, Endian::Big)?;
|
||||
log::debug!("Loaded .comment section header {:?}", header);
|
||||
CommentSym::from_reader(&mut reader, Endian::Big)?; // Null symbol
|
||||
let mut comment_syms = Vec::with_capacity(obj_file.symbols().count());
|
||||
for symbol in obj_file.symbols() {
|
||||
let comment_sym = CommentSym::from_reader(&mut reader, Endian::Big)?;
|
||||
log::debug!("Symbol {:?} -> Comment {:?}", symbol, comment_sym);
|
||||
comment_syms.push(comment_sym);
|
||||
}
|
||||
ensure!(data.len() == reader.position() as usize, "Section data not fully read");
|
||||
Ok(Some((header, comment_syms)))
|
||||
}
|
||||
|
||||
pub fn write_elf(obj: &ObjInfo, export_all: bool) -> Result<Vec<u8>> {
|
||||
let mut out_data = Vec::new();
|
||||
let mut writer = Writer::new(Endianness::Big, false, &mut out_data);
|
||||
@ -862,7 +861,7 @@ fn to_obj_symbol(
|
||||
obj_file: &object::File<'_>,
|
||||
symbol: &Symbol<'_, '_>,
|
||||
section_indexes: &[Option<usize>],
|
||||
comment_sym: Option<&CommentSym>,
|
||||
align: Option<u32>,
|
||||
) -> Result<ObjSymbol> {
|
||||
let section = match symbol.section_index() {
|
||||
Some(idx) => Some(obj_file.section_by_index(idx)?),
|
||||
@ -892,9 +891,6 @@ fn to_obj_symbol(
|
||||
if symbol.scope() == SymbolScope::Linkage {
|
||||
flags = ObjSymbolFlagSet(flags.0 | ObjSymbolFlags::Hidden);
|
||||
}
|
||||
if comment_sym.is_some_and(|c| c.active_flags & 0x8 != 0) {
|
||||
flags = ObjSymbolFlagSet(flags.0 | ObjSymbolFlags::Exported);
|
||||
}
|
||||
let section_idx = section.as_ref().and_then(|section| section_indexes[section.index().0]);
|
||||
Ok(ObjSymbol {
|
||||
name: name.to_string(),
|
||||
@ -911,7 +907,7 @@ fn to_obj_symbol(
|
||||
SymbolKind::Section => ObjSymbolKind::Section,
|
||||
_ => bail!("Unsupported symbol kind: {:?}", symbol),
|
||||
},
|
||||
align: comment_sym.map(|c| c.align),
|
||||
align,
|
||||
..Default::default()
|
||||
})
|
||||
}
|
||||
@ -1009,10 +1005,3 @@ fn write_relocatable_section_data(w: &mut Writer, section: &ObjSection) -> Resul
|
||||
w.write(§ion.data[current_address..]);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn is_elf_file(path: &Utf8NativePathBuf) -> Result<bool> {
|
||||
let mut file = open_file(path, true)?;
|
||||
let mut magic = [0; 4];
|
||||
file.read_exact(&mut magic)?;
|
||||
Ok(magic == elf::ELFMAG)
|
||||
}
|
||||
|
@ -1,57 +0,0 @@
|
||||
use anyhow::{Context, Result};
|
||||
use itertools::Itertools;
|
||||
|
||||
use crate::obj::ObjInfo;
|
||||
|
||||
pub fn clean_extab(obj: &mut ObjInfo, mut padding: impl Iterator<Item = u8>) -> Result<usize> {
|
||||
let (extab_section_index, extab_section) = obj
|
||||
.sections
|
||||
.iter_mut()
|
||||
.find(|(_, s)| s.name == "extab")
|
||||
.ok_or_else(|| anyhow::anyhow!("No extab section found"))?;
|
||||
let mut num_cleaned = 0;
|
||||
for (_symbol_index, symbol) in obj
|
||||
.symbols
|
||||
.for_section(extab_section_index)
|
||||
.filter(|(_, s)| s.size > 0)
|
||||
.sorted_by_key(|(_, s)| s.address)
|
||||
{
|
||||
let data = extab_section.symbol_data(symbol)?;
|
||||
let decoded = cwextab::decode_extab(data).with_context(|| {
|
||||
format!(
|
||||
"Failed to decode {} (extab {:#010X}..{:#010X})",
|
||||
symbol.name,
|
||||
symbol.address,
|
||||
symbol.address + symbol.size
|
||||
)
|
||||
})?;
|
||||
let mut updated = false;
|
||||
for action in &decoded.exception_actions {
|
||||
// Check if the current action has padding
|
||||
if let Some(padding_offset) = action.get_struct_padding_offset() {
|
||||
let index = padding_offset as usize;
|
||||
let section_offset = (symbol.address - extab_section.address) as usize
|
||||
+ action.action_offset as usize;
|
||||
let mut clean_data: Vec<u8> = action.get_exaction_bytes(false);
|
||||
// Write the two padding bytes
|
||||
clean_data[index] = padding.next().unwrap_or(0);
|
||||
clean_data[index + 1] = padding.next().unwrap_or(0);
|
||||
|
||||
let orig_data =
|
||||
&mut extab_section.data[section_offset..section_offset + clean_data.len()];
|
||||
orig_data.copy_from_slice(&clean_data);
|
||||
updated = true;
|
||||
}
|
||||
}
|
||||
if updated {
|
||||
tracing::debug!(
|
||||
"Replaced uninitialized bytes in {} (extab {:#010X}..{:#010X})",
|
||||
symbol.name,
|
||||
symbol.address,
|
||||
symbol.address + symbol.size
|
||||
);
|
||||
num_cleaned += 1;
|
||||
}
|
||||
}
|
||||
Ok(num_cleaned)
|
||||
}
|
@ -26,7 +26,7 @@ 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}'"))?;
|
||||
let file = File::create(path).with_context(|| format!("Failed to create file '{}'", path))?;
|
||||
Ok(BufWriter::new(file))
|
||||
}
|
||||
|
||||
|
@ -47,11 +47,11 @@ pub fn generate_ldscript(
|
||||
|
||||
let out = template
|
||||
.unwrap_or(LCF_TEMPLATE)
|
||||
.replace("$ORIGIN", &format!("{origin:#X}"))
|
||||
.replace("$ORIGIN", &format!("{:#X}", origin))
|
||||
.replace("$SECTIONS", §ion_defs)
|
||||
.replace("$LAST_SECTION_SYMBOL", &last_section_symbol)
|
||||
.replace("$LAST_SECTION_NAME", &last_section_name)
|
||||
.replace("$STACKSIZE", &format!("{stack_size:#X}"))
|
||||
.replace("$STACKSIZE", &format!("{:#X}", stack_size))
|
||||
.replace("$FORCEACTIVE", &force_active.join("\n "))
|
||||
.replace("$ARENAHI", &format!("{:#X}", obj.arena_hi.unwrap_or(0x81700000)));
|
||||
Ok(out)
|
||||
@ -74,7 +74,7 @@ pub fn generate_ldscript_partial(
|
||||
// Some RELs have no entry point (`.text` was stripped) so mwld requires at least an empty
|
||||
// `.init` section to be present in the linker script, for some reason.
|
||||
if obj.entry.is_none() {
|
||||
section_defs = format!(".init :{{}}\n {section_defs}");
|
||||
section_defs = format!(".init :{{}}\n {}", section_defs);
|
||||
}
|
||||
|
||||
let mut force_files = Vec::with_capacity(obj.link_order.len());
|
||||
|
@ -802,7 +802,10 @@ pub fn apply_map(mut result: MapInfo, obj: &mut ObjInfo) -> Result<()> {
|
||||
}
|
||||
ObjSectionKind::Data | ObjSectionKind::ReadOnlyData => {
|
||||
if section.elf_index == 4 {
|
||||
if result.section_symbols.get(".rodata").is_some_and(|m| !m.is_empty())
|
||||
if result
|
||||
.section_symbols
|
||||
.get(".rodata")
|
||||
.map_or(false, |m| !m.is_empty())
|
||||
{
|
||||
".rodata"
|
||||
} else {
|
||||
|
@ -10,7 +10,6 @@ pub mod diff;
|
||||
pub mod dol;
|
||||
pub mod dwarf;
|
||||
pub mod elf;
|
||||
pub mod extab;
|
||||
pub mod file;
|
||||
pub mod lcf;
|
||||
pub mod map;
|
||||
@ -19,16 +18,13 @@ pub mod nested;
|
||||
pub mod nlzss;
|
||||
pub mod path;
|
||||
pub mod rarc;
|
||||
pub mod read;
|
||||
pub mod reader;
|
||||
pub mod rel;
|
||||
pub mod rso;
|
||||
pub mod signatures;
|
||||
pub mod split;
|
||||
pub mod take_seek;
|
||||
pub mod toposort;
|
||||
pub mod u8_arc;
|
||||
pub mod wad;
|
||||
|
||||
#[inline]
|
||||
pub const fn align_up(value: u32, align: u32) -> u32 { (value + (align - 1)) & !(align - 1) }
|
||||
@ -96,7 +92,7 @@ pub enum Bytes<'a> {
|
||||
Owned(Box<[u8]>),
|
||||
}
|
||||
|
||||
impl Bytes<'_> {
|
||||
impl<'a> Bytes<'a> {
|
||||
pub fn into_owned(self) -> Box<[u8]> {
|
||||
match self {
|
||||
Bytes::Borrowed(s) => Box::from(s),
|
||||
@ -105,7 +101,7 @@ impl Bytes<'_> {
|
||||
}
|
||||
}
|
||||
|
||||
impl AsRef<[u8]> for Bytes<'_> {
|
||||
impl<'a> AsRef<[u8]> for Bytes<'a> {
|
||||
fn as_ref(&self) -> &[u8] {
|
||||
match self {
|
||||
Bytes::Borrowed(s) => s,
|
||||
|
@ -209,7 +209,7 @@ impl<'a> RarcView<'a> {
|
||||
)
|
||||
})?;
|
||||
let c_string = CStr::from_bytes_until_nul(name_buf)
|
||||
.map_err(|_| format!("RARC: name at offset {offset} not null-terminated"))?;
|
||||
.map_err(|_| format!("RARC: name at offset {} not null-terminated", offset))?;
|
||||
Ok(c_string.to_string_lossy())
|
||||
}
|
||||
|
||||
|
@ -1,26 +0,0 @@
|
||||
use std::{io, io::Read};
|
||||
|
||||
use zerocopy::{FromBytes, FromZeros, IntoBytes};
|
||||
|
||||
#[inline(always)]
|
||||
pub fn read_from<T, R>(reader: &mut R) -> io::Result<T>
|
||||
where
|
||||
T: FromBytes + IntoBytes,
|
||||
R: Read + ?Sized,
|
||||
{
|
||||
let mut ret = <T>::new_zeroed();
|
||||
reader.read_exact(ret.as_mut_bytes())?;
|
||||
Ok(ret)
|
||||
}
|
||||
|
||||
#[inline(always)]
|
||||
pub fn read_box_slice<T, R>(reader: &mut R, count: usize) -> io::Result<Box<[T]>>
|
||||
where
|
||||
T: FromBytes + IntoBytes,
|
||||
R: Read + ?Sized,
|
||||
{
|
||||
let mut ret = <[T]>::new_box_zeroed_with_elems(count)
|
||||
.map_err(|_| io::Error::from(io::ErrorKind::OutOfMemory))?;
|
||||
reader.read_exact(ret.as_mut().as_mut_bytes())?;
|
||||
Ok(ret)
|
||||
}
|
@ -20,15 +20,6 @@ impl From<object::Endianness> for Endian {
|
||||
}
|
||||
}
|
||||
|
||||
impl Endian {
|
||||
pub fn flip(self) -> Self {
|
||||
match self {
|
||||
Endian::Big => Endian::Little,
|
||||
Endian::Little => Endian::Big,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub const DYNAMIC_SIZE: usize = 0;
|
||||
|
||||
pub const fn struct_size<const N: usize>(fields: [usize; N]) -> usize {
|
||||
@ -272,18 +263,6 @@ impl ToWriter for Vec<u8> {
|
||||
fn write_size(&self) -> usize { self.len() }
|
||||
}
|
||||
|
||||
impl<const N: usize> ToWriter for [u32; N] {
|
||||
fn to_writer<W>(&self, writer: &mut W, e: Endian) -> io::Result<()>
|
||||
where W: Write + ?Sized {
|
||||
for &value in self {
|
||||
value.to_writer(writer, e)?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn write_size(&self) -> usize { N * u32::STATIC_SIZE }
|
||||
}
|
||||
|
||||
pub fn write_vec<T, W>(writer: &mut W, vec: &[T], e: Endian) -> io::Result<()>
|
||||
where
|
||||
T: ToWriter,
|
||||
|
@ -364,7 +364,7 @@ where
|
||||
reader.seek(SeekFrom::Start(header.section_info_offset as u64))?;
|
||||
for idx in 0..header.num_sections {
|
||||
let section = RelSectionHeader::from_reader(reader, Endian::Big)
|
||||
.with_context(|| format!("Failed to read REL section header {idx}"))?;
|
||||
.with_context(|| format!("Failed to read REL section header {}", idx))?;
|
||||
sections.push(section);
|
||||
}
|
||||
Ok(sections)
|
||||
@ -390,7 +390,7 @@ where R: Read + Seek + ?Sized {
|
||||
reader.seek(SeekFrom::Start(offset as u64))?;
|
||||
let mut data = vec![0u8; size as usize];
|
||||
reader.read_exact(&mut data).with_context(|| {
|
||||
format!("Failed to read REL section {idx} data with size {size:#X}")
|
||||
format!("Failed to read REL section {} data with size {:#X}", idx, size)
|
||||
})?;
|
||||
reader.seek(SeekFrom::Start(position))?;
|
||||
data
|
||||
@ -405,7 +405,7 @@ where R: Read + Seek + ?Sized {
|
||||
text_section = Some(idx as u8);
|
||||
(".text".to_string(), ObjSectionKind::Code, true)
|
||||
} else {
|
||||
(format!(".section{idx}"), ObjSectionKind::Data, false)
|
||||
(format!(".section{}", idx), ObjSectionKind::Data, false)
|
||||
};
|
||||
sections.push(ObjSection {
|
||||
name,
|
||||
|
@ -147,14 +147,14 @@ impl FromReader for RsoHeader {
|
||||
if next != 0 {
|
||||
return Err(io::Error::new(
|
||||
io::ErrorKind::InvalidData,
|
||||
format!("Expected 'next' to be 0, got {next:#X}"),
|
||||
format!("Expected 'next' to be 0, got {:#X}", next),
|
||||
));
|
||||
}
|
||||
let prev = u32::from_reader(reader, e)?;
|
||||
if prev != 0 {
|
||||
return Err(io::Error::new(
|
||||
io::ErrorKind::InvalidData,
|
||||
format!("Expected 'prev' to be 0, got {prev:#X}"),
|
||||
format!("Expected 'prev' to be 0, got {:#X}", prev),
|
||||
));
|
||||
}
|
||||
let num_sections = u32::from_reader(reader, e)?;
|
||||
@ -170,7 +170,7 @@ impl FromReader for RsoHeader {
|
||||
if bss_section != 0 {
|
||||
return Err(io::Error::new(
|
||||
io::ErrorKind::InvalidData,
|
||||
format!("Expected 'bssSection' to be 0, got {bss_section:#X}"),
|
||||
format!("Expected 'bssSection' to be 0, got {:#X}", bss_section),
|
||||
));
|
||||
}
|
||||
let prolog_offset = u32::from_reader(reader, e)?;
|
||||
@ -440,7 +440,7 @@ where R: Read + Seek + ?Sized {
|
||||
// println!("Section {} offset {:#X} size {:#X}", idx, offset, size);
|
||||
|
||||
sections.push(ObjSection {
|
||||
name: format!(".section{idx}"),
|
||||
name: format!(".section{}", idx),
|
||||
kind: if offset == 0 {
|
||||
ObjSectionKind::Bss
|
||||
} else if section.exec() {
|
||||
|
@ -6,6 +6,7 @@ use std::{
|
||||
use anyhow::{anyhow, bail, ensure, Context, Result};
|
||||
use itertools::Itertools;
|
||||
use objdiff_core::obj::split_meta::SplitMeta;
|
||||
use petgraph::{graph::NodeIndex, Graph};
|
||||
use sanitise_file_name::sanitize_with_options;
|
||||
use tracing_attributes::instrument;
|
||||
|
||||
@ -16,7 +17,7 @@ use crate::{
|
||||
ObjSplit, ObjSymbol, ObjSymbolFlagSet, ObjSymbolFlags, ObjSymbolKind, ObjSymbolScope,
|
||||
ObjUnit, SectionIndex, SymbolIndex,
|
||||
},
|
||||
util::{align_up, comment::MWComment, toposort::toposort},
|
||||
util::{align_up, comment::MWComment},
|
||||
};
|
||||
|
||||
/// Create splits for function pointers in the given section.
|
||||
@ -26,25 +27,7 @@ fn split_ctors_dtors(obj: &mut ObjInfo, start: SectionAddress, end: SectionAddre
|
||||
let mut current_address = start;
|
||||
let mut referenced_symbols = vec![];
|
||||
|
||||
// ProDG ctor list can start with -1
|
||||
if matches!(read_u32(ctors_section, current_address.address), Some(0xFFFFFFFF)) {
|
||||
current_address += 4;
|
||||
}
|
||||
|
||||
while current_address < end {
|
||||
// ProDG hack when the end address is not known
|
||||
if matches!(read_u32(ctors_section, current_address.address), Some(0)) {
|
||||
while current_address < end {
|
||||
ensure!(
|
||||
matches!(read_u32(ctors_section, current_address.address), Some(0)),
|
||||
"{} data detected at {:#010X} after null pointer",
|
||||
ctors_section.name,
|
||||
current_address,
|
||||
);
|
||||
current_address += 4;
|
||||
}
|
||||
break;
|
||||
}
|
||||
let function_addr = read_address(obj, ctors_section, current_address.address)?;
|
||||
log::debug!("Found {} entry: {:#010X}", ctors_section.name, function_addr);
|
||||
|
||||
@ -403,20 +386,6 @@ fn create_gap_splits(obj: &mut ObjInfo) -> Result<()> {
|
||||
new_split_end.address = symbol.address as u32;
|
||||
break;
|
||||
}
|
||||
// Create a new split if we need to adjust the alignment
|
||||
if let Some(align) = symbol.align {
|
||||
if current_address & (align - 1) != 0 {
|
||||
log::debug!(
|
||||
"Auto split {:#010X}..{:#010X} for symbol {} with alignment {}",
|
||||
current_address,
|
||||
symbol.address,
|
||||
symbol.name,
|
||||
align
|
||||
);
|
||||
new_split_end.address = symbol.address as u32;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ensure!(
|
||||
@ -561,9 +530,8 @@ fn validate_splits(obj: &ObjInfo) -> Result<()> {
|
||||
|
||||
/// Add padding symbols to fill in gaps between splits and symbols.
|
||||
fn add_padding_symbols(obj: &mut ObjInfo) -> Result<()> {
|
||||
let mut splits = obj.sections.all_splits().peekable();
|
||||
while let Some((section_index, section, addr, split)) = splits.next() {
|
||||
if section.name == ".ctors" || section.name == ".dtors" || addr == split.end {
|
||||
for (section_index, section, addr, _split) in obj.sections.all_splits() {
|
||||
if section.name == ".ctors" || section.name == ".dtors" {
|
||||
continue;
|
||||
}
|
||||
|
||||
@ -577,32 +545,19 @@ fn add_padding_symbols(obj: &mut ObjInfo) -> Result<()> {
|
||||
})?
|
||||
.is_none()
|
||||
{
|
||||
let next_split_address = splits
|
||||
.peek()
|
||||
.filter(|(i, _, _, _)| *i == section_index)
|
||||
.map(|(_, _, addr, _)| *addr as u64)
|
||||
.unwrap_or(section.address + section.size);
|
||||
let next_symbol_address = obj
|
||||
.symbols
|
||||
.for_section_range(section_index, addr + 1..next_split_address as u32)
|
||||
.for_section_range(section_index, addr + 1..)
|
||||
.find(|&(_, s)| s.size_known && s.size > 0)
|
||||
.map(|(_, s)| s.address)
|
||||
.unwrap_or(next_split_address);
|
||||
if next_symbol_address <= addr as u64 {
|
||||
continue;
|
||||
}
|
||||
.unwrap_or(section.address + section.size);
|
||||
let symbol_name = format!(
|
||||
"pad_{:02}_{:08X}_{}",
|
||||
section_index,
|
||||
addr,
|
||||
section.name.trim_start_matches('.')
|
||||
);
|
||||
log::debug!(
|
||||
"Adding padding symbol {} at {:#010X}-{:#010X}",
|
||||
symbol_name,
|
||||
addr,
|
||||
next_symbol_address
|
||||
);
|
||||
log::debug!("Adding padding symbol {} at {:#010X}", symbol_name, addr);
|
||||
obj.symbols.add_direct(ObjSymbol {
|
||||
name: symbol_name,
|
||||
address: addr as u64,
|
||||
@ -647,56 +602,20 @@ fn add_padding_symbols(obj: &mut ObjInfo) -> Result<()> {
|
||||
if let Some(&(_, next_symbol)) = iter.peek() {
|
||||
(
|
||||
next_symbol.name.as_str(),
|
||||
next_symbol.address as u32,
|
||||
(next_symbol.address + next_symbol.size) as u32,
|
||||
next_symbol.address,
|
||||
next_symbol.address + next_symbol.size,
|
||||
next_symbol.align.unwrap_or(1),
|
||||
)
|
||||
} else {
|
||||
(
|
||||
section.name.as_str(),
|
||||
(section.address + section.size) as u32,
|
||||
(section.address + section.size) as u32,
|
||||
section.address + section.size,
|
||||
section.address + section.size,
|
||||
1,
|
||||
)
|
||||
};
|
||||
|
||||
// Check if symbol is missing data between the end of the symbol and the next symbol
|
||||
let symbol_end = (symbol.address + symbol.size) as u32;
|
||||
if !matches!(section.kind, ObjSectionKind::Code | ObjSectionKind::Bss)
|
||||
&& next_address > symbol_end
|
||||
{
|
||||
let data = section.data_range(symbol_end, next_address)?;
|
||||
if data.iter().any(|&x| x != 0) {
|
||||
log::debug!(
|
||||
"Non-zero data between {:#010X}..{:#010X}, creating new symbol",
|
||||
symbol_end,
|
||||
next_address
|
||||
);
|
||||
let name = if obj.module_id == 0 {
|
||||
format!("lbl_{symbol_end:08X}")
|
||||
} else {
|
||||
format!(
|
||||
"lbl_{}_{}_{:X}",
|
||||
obj.module_id,
|
||||
section.name.trim_start_matches('.'),
|
||||
symbol_end
|
||||
)
|
||||
};
|
||||
to_add.push(ObjSymbol {
|
||||
name,
|
||||
address: symbol_end as u64,
|
||||
section: Some(section_index),
|
||||
size: (next_address - symbol_end) as u64,
|
||||
size_known: true,
|
||||
kind: ObjSymbolKind::Object,
|
||||
..Default::default()
|
||||
});
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
let aligned_end = align_up(symbol_end, next_align);
|
||||
match aligned_end.cmp(&next_address) {
|
||||
let aligned_end = align_up((symbol.address + symbol.size) as u32, next_align);
|
||||
match aligned_end.cmp(&(next_address as u32)) {
|
||||
Ordering::Less => {
|
||||
let symbol_name = format!(
|
||||
"gap_{:02}_{:08X}_{}",
|
||||
@ -709,7 +628,7 @@ fn add_padding_symbols(obj: &mut ObjInfo) -> Result<()> {
|
||||
name: symbol_name,
|
||||
address: aligned_end as u64,
|
||||
section: Some(section_index),
|
||||
size: (next_address - aligned_end) as u64,
|
||||
size: next_address - aligned_end as u64,
|
||||
size_known: true,
|
||||
flags: ObjSymbolFlagSet(
|
||||
ObjSymbolFlags::Global
|
||||
@ -907,16 +826,14 @@ fn resolve_link_order(obj: &ObjInfo) -> Result<Vec<ObjUnit>> {
|
||||
to: i64,
|
||||
}
|
||||
|
||||
let mut unit_to_index_map = BTreeMap::<&str, usize>::new();
|
||||
let mut index_to_unit = vec![];
|
||||
let mut graph = Graph::<String, SplitEdge>::new();
|
||||
let mut unit_to_index_map = BTreeMap::<String, NodeIndex>::new();
|
||||
for (_, _, _, split) in obj.sections.all_splits() {
|
||||
unit_to_index_map.entry(split.unit.as_str()).or_insert_with(|| {
|
||||
let idx = index_to_unit.len();
|
||||
index_to_unit.push(split.unit.as_str());
|
||||
idx
|
||||
});
|
||||
unit_to_index_map.insert(split.unit.clone(), NodeIndex::new(0));
|
||||
}
|
||||
for (unit, index) in unit_to_index_map.iter_mut() {
|
||||
*index = graph.add_node(unit.clone());
|
||||
}
|
||||
let mut graph = vec![vec![]; index_to_unit.len()];
|
||||
|
||||
for (_section_index, section) in obj.sections.iter() {
|
||||
let mut iter = section.splits.iter().peekable();
|
||||
@ -939,9 +856,12 @@ fn resolve_link_order(obj: &ObjInfo) -> Result<Vec<ObjUnit>> {
|
||||
b.unit,
|
||||
b_addr
|
||||
);
|
||||
let a_index = *unit_to_index_map.get(a.unit.as_str()).unwrap();
|
||||
let b_index = *unit_to_index_map.get(b.unit.as_str()).unwrap();
|
||||
graph[a_index].push(b_index);
|
||||
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 as i64,
|
||||
to: b_addr as i64,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -960,22 +880,41 @@ fn resolve_link_order(obj: &ObjInfo) -> Result<Vec<ObjUnit>> {
|
||||
}
|
||||
}
|
||||
}
|
||||
let mut iter =
|
||||
ordered_units.values().filter_map(|unit| unit_to_index_map.get(unit.as_str())).peekable();
|
||||
while let (Some(&a_index), Some(&&b_index)) = (iter.next(), iter.peek()) {
|
||||
graph[a_index].push(b_index);
|
||||
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 });
|
||||
}
|
||||
|
||||
match toposort(&graph) {
|
||||
// use petgraph::{
|
||||
// dot::{Config, Dot},
|
||||
// graph::EdgeReference,
|
||||
// };
|
||||
// let get_edge_attributes = |_, e: EdgeReference<SplitEdge>| {
|
||||
// let &SplitEdge { from, to } = e.weight();
|
||||
// let section_name = &obj.section_at(from).unwrap().name;
|
||||
// format!("label=\"{} {:#010X} -> {:#010X}\"", section_name, from, to)
|
||||
// };
|
||||
// let dot = Dot::with_attr_getters(
|
||||
// &graph,
|
||||
// &[Config::EdgeNoLabel, Config::NodeNoLabel],
|
||||
// &get_edge_attributes,
|
||||
// &|_, (_, s)| format!("label=\"{}\"", s),
|
||||
// );
|
||||
// println!("{:?}", dot);
|
||||
|
||||
match petgraph::algo::toposort(&graph, None) {
|
||||
Ok(vec) => Ok(vec
|
||||
.iter()
|
||||
.map(|&idx| {
|
||||
let name = index_to_unit[idx];
|
||||
if let Some(existing) = obj.link_order.iter().find(|u| u.name == name) {
|
||||
let name = &graph[idx];
|
||||
if let Some(existing) = obj.link_order.iter().find(|u| &u.name == name) {
|
||||
existing.clone()
|
||||
} else {
|
||||
ObjUnit {
|
||||
name: name.to_string(),
|
||||
name: name.clone(),
|
||||
autogenerated: obj.is_unit_autogenerated(name),
|
||||
comment_version: None,
|
||||
order: None,
|
||||
@ -984,8 +923,8 @@ fn resolve_link_order(obj: &ObjInfo) -> Result<Vec<ObjUnit>> {
|
||||
})
|
||||
.collect_vec()),
|
||||
Err(e) => Err(anyhow!(
|
||||
"Cyclic dependency encountered while resolving link order: {}",
|
||||
e.iter().map(|&idx| index_to_unit[idx]).join(" -> ")
|
||||
"Cyclic dependency (involving {}) encountered while resolving link order",
|
||||
graph[e.node_id()]
|
||||
)),
|
||||
}
|
||||
}
|
||||
@ -1095,15 +1034,13 @@ pub fn split_obj(obj: &ObjInfo, module_name: Option<&str>) -> Result<Vec<ObjInfo
|
||||
}) as u64;
|
||||
|
||||
if current_address & (align as u32 - 1) != 0 {
|
||||
if !split.autogenerated {
|
||||
log::warn!(
|
||||
"Alignment for {} {} expected {}, but starts at {:#010X}",
|
||||
split.unit,
|
||||
section.name,
|
||||
align,
|
||||
current_address
|
||||
);
|
||||
}
|
||||
log::warn!(
|
||||
"Alignment for {} {} expected {}, but starts at {:#010X}",
|
||||
split.unit,
|
||||
section.name,
|
||||
align,
|
||||
current_address
|
||||
);
|
||||
while align > 4 {
|
||||
align /= 2;
|
||||
if current_address & (align as u32 - 1) == 0 {
|
||||
@ -1483,7 +1420,7 @@ fn auto_unit_name(
|
||||
if unit_exists(&unit_name, obj, new_splits) {
|
||||
let mut i = 1;
|
||||
loop {
|
||||
let new_unit_name = format!("{unit_name}_{i}");
|
||||
let new_unit_name = format!("{}_{}", unit_name, i);
|
||||
if !unit_exists(&new_unit_name, obj, new_splits) {
|
||||
unit_name = new_unit_name;
|
||||
break;
|
||||
|
@ -1,117 +0,0 @@
|
||||
/// Topological sort algorithm based on DFS:
|
||||
/// https://en.wikipedia.org/wiki/Topological_sorting#Depth-first_search
|
||||
/// Finds either an ordering of the vertices such that all edges go from
|
||||
/// lower to higher indexed nodes, or a cycle.
|
||||
///
|
||||
/// Implementation by Simon Lindholm
|
||||
/// https://gist.github.com/simonlindholm/08664dd783ad4b9f23532fdd5e352b42
|
||||
pub fn toposort(graph: &[Vec<usize>]) -> Result<Vec<usize>, Vec<usize>> {
|
||||
let n = graph.len();
|
||||
#[derive(Copy, Clone)]
|
||||
enum State {
|
||||
Unvisited,
|
||||
Active(usize, usize),
|
||||
Finished,
|
||||
}
|
||||
let mut state = vec![State::Unvisited; n + 1];
|
||||
state[n] = State::Active(0, usize::MAX);
|
||||
let mut ret = Vec::new();
|
||||
let mut cur = n;
|
||||
loop {
|
||||
let State::Active(eind, par) = state[cur] else { panic!("unexpected state 1") };
|
||||
let adj;
|
||||
if cur == n {
|
||||
if eind == n {
|
||||
break;
|
||||
}
|
||||
adj = eind;
|
||||
} else {
|
||||
if eind == graph[cur].len() {
|
||||
state[cur] = State::Finished;
|
||||
ret.push(cur);
|
||||
cur = par;
|
||||
continue;
|
||||
}
|
||||
adj = graph[cur][eind];
|
||||
};
|
||||
state[cur] = State::Active(eind + 1, par);
|
||||
match state[adj] {
|
||||
State::Unvisited => {
|
||||
state[adj] = State::Active(0, cur);
|
||||
cur = adj;
|
||||
}
|
||||
State::Active(..) => {
|
||||
let mut cycle = Vec::new();
|
||||
while cur != adj {
|
||||
cycle.push(cur);
|
||||
let State::Active(_, par) = state[cur] else { panic!("unexpected state 2") };
|
||||
cur = par;
|
||||
}
|
||||
cycle.push(cur);
|
||||
cycle.reverse();
|
||||
return Err(cycle);
|
||||
}
|
||||
State::Finished => {}
|
||||
};
|
||||
}
|
||||
ret.reverse();
|
||||
Ok(ret)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use std::collections::HashSet;
|
||||
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_correct() {
|
||||
let mut rng_state: usize = 0;
|
||||
let mut rand = || {
|
||||
rng_state = rng_state.wrapping_mul(123156351724123181_usize);
|
||||
rng_state = rng_state.wrapping_add(670143798154186239_usize);
|
||||
rng_state >> 32
|
||||
};
|
||||
for _ in 0..10000 {
|
||||
let n = rand() % 20;
|
||||
let mut g = vec![vec![]; n];
|
||||
let mut g_set = HashSet::new();
|
||||
if n != 0 {
|
||||
let m = rand() % 50;
|
||||
for _ in 0..m {
|
||||
let a = rand() % n;
|
||||
let b = rand() % n;
|
||||
g[a].push(b);
|
||||
g_set.insert((a, b));
|
||||
}
|
||||
}
|
||||
match toposort(&g) {
|
||||
Ok(order) => {
|
||||
assert_eq!(order.len(), n);
|
||||
let mut node_to_order = vec![usize::MAX; n];
|
||||
// Every node should occur exactly once...
|
||||
for (i, &node) in order.iter().enumerate() {
|
||||
assert!(node < n);
|
||||
assert_eq!(node_to_order[node], usize::MAX);
|
||||
node_to_order[node] = i;
|
||||
}
|
||||
// and the edges should go in forward order in the list
|
||||
for i in 0..n {
|
||||
for &j in &g[i] {
|
||||
assert!(node_to_order[i] < node_to_order[j]);
|
||||
}
|
||||
}
|
||||
}
|
||||
Err(cycle) => {
|
||||
// The found cycle should exist in the graph
|
||||
assert!(!cycle.is_empty());
|
||||
for i in 0..cycle.len() {
|
||||
let a = cycle[i];
|
||||
let b = cycle[(i + 1) % cycle.len()];
|
||||
assert!(g_set.contains(&(a, b)));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -149,7 +149,7 @@ impl<'a> U8View<'a> {
|
||||
let mut idx = 1;
|
||||
let mut stop_at = None;
|
||||
while let Some(node) = self.nodes.get(idx).copied() {
|
||||
if self.get_name(node).is_ok_and(|name| name.eq_ignore_ascii_case(current)) {
|
||||
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));
|
||||
|
220
src/util/wad.rs
220
src/util/wad.rs
@ -1,220 +0,0 @@
|
||||
use std::{
|
||||
io,
|
||||
io::{BufRead, Read, Seek},
|
||||
};
|
||||
|
||||
use aes::cipher::{BlockDecryptMut, KeyIvInit};
|
||||
use anyhow::{bail, Result};
|
||||
use nodtool::nod::{Ticket, TmdHeader};
|
||||
use sha1::{Digest, Sha1};
|
||||
use size::Size;
|
||||
use zerocopy::{big_endian::*, FromBytes, FromZeros, Immutable, IntoBytes, KnownLayout};
|
||||
|
||||
use crate::{
|
||||
array_ref_mut, static_assert,
|
||||
util::read::{read_box_slice, read_from},
|
||||
};
|
||||
|
||||
// TODO: other WAD types?
|
||||
pub const WAD_MAGIC: [u8; 8] = [0x00, 0x00, 0x00, 0x20, 0x49, 0x73, 0x00, 0x00];
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, FromBytes, IntoBytes, Immutable, KnownLayout)]
|
||||
#[repr(C, align(4))]
|
||||
pub struct WadHeader {
|
||||
pub header_size: U32,
|
||||
pub wad_type: [u8; 0x2],
|
||||
pub wad_version: U16,
|
||||
pub cert_chain_size: U32,
|
||||
pub _reserved1: [u8; 0x4],
|
||||
pub ticket_size: U32,
|
||||
pub tmd_size: U32,
|
||||
pub data_size: U32,
|
||||
pub footer_size: U32,
|
||||
}
|
||||
|
||||
static_assert!(size_of::<WadHeader>() == 0x20);
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, FromBytes, IntoBytes, Immutable, KnownLayout)]
|
||||
#[repr(C, align(4))]
|
||||
pub struct ContentMetadata {
|
||||
pub content_id: U32,
|
||||
pub content_index: U16,
|
||||
pub content_type: U16,
|
||||
pub size: U64,
|
||||
pub hash: HashBytes,
|
||||
}
|
||||
|
||||
static_assert!(size_of::<ContentMetadata>() == 0x24);
|
||||
|
||||
impl ContentMetadata {
|
||||
#[inline]
|
||||
pub fn iv(&self) -> [u8; 0x10] {
|
||||
let mut iv = [0u8; 0x10];
|
||||
*array_ref_mut!(iv, 0, 2) = self.content_index.get().to_be_bytes();
|
||||
iv
|
||||
}
|
||||
}
|
||||
|
||||
const ALIGNMENT: usize = 0x40;
|
||||
|
||||
#[inline(always)]
|
||||
pub fn align_up(value: u64, alignment: u64) -> u64 { (value + alignment - 1) & !(alignment - 1) }
|
||||
|
||||
pub type HashBytes = [u8; 20];
|
||||
pub type KeyBytes = [u8; 16];
|
||||
|
||||
type Aes128Cbc = cbc::Decryptor<aes::Aes128>;
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct WadFile {
|
||||
pub header: WadHeader,
|
||||
pub title_key: KeyBytes,
|
||||
pub fake_signed: bool,
|
||||
pub raw_cert_chain: Box<[u8]>,
|
||||
pub raw_ticket: Box<[u8]>,
|
||||
pub raw_tmd: Box<[u8]>,
|
||||
pub content_offset: u64,
|
||||
}
|
||||
|
||||
impl WadFile {
|
||||
pub fn ticket(&self) -> &Ticket {
|
||||
Ticket::ref_from_bytes(&self.raw_ticket).expect("Invalid ticket alignment")
|
||||
}
|
||||
|
||||
pub fn tmd(&self) -> &TmdHeader {
|
||||
TmdHeader::ref_from_prefix(&self.raw_tmd).expect("Invalid TMD alignment").0
|
||||
}
|
||||
|
||||
pub fn contents(&self) -> &[ContentMetadata] {
|
||||
let (_, cmd_data) =
|
||||
TmdHeader::ref_from_prefix(&self.raw_tmd).expect("Invalid TMD alignment");
|
||||
<[ContentMetadata]>::ref_from_bytes(cmd_data).expect("Invalid CMD alignment")
|
||||
}
|
||||
|
||||
pub fn content_offset(&self, content_index: u16) -> u64 {
|
||||
let contents = self.contents();
|
||||
let mut offset = self.content_offset;
|
||||
for content in contents.iter().take(content_index as usize) {
|
||||
offset = align_up(offset + content.size.get(), ALIGNMENT as u64);
|
||||
}
|
||||
offset
|
||||
}
|
||||
|
||||
pub fn trailer_offset(&self) -> u64 {
|
||||
let contents = self.contents();
|
||||
let mut offset = self.content_offset;
|
||||
for content in contents.iter() {
|
||||
offset = align_up(offset + content.size.get(), ALIGNMENT as u64);
|
||||
}
|
||||
offset
|
||||
}
|
||||
}
|
||||
|
||||
pub fn process_wad<R>(reader: &mut R) -> Result<WadFile>
|
||||
where R: BufRead + Seek + ?Sized {
|
||||
let header: WadHeader = read_from(reader)?;
|
||||
let mut offset = align_up(header.header_size.get() as u64, ALIGNMENT as u64);
|
||||
|
||||
reader.seek(io::SeekFrom::Start(offset))?;
|
||||
let raw_cert_chain: Box<[u8]> = read_box_slice(reader, header.cert_chain_size.get() as usize)?;
|
||||
offset = align_up(offset + header.cert_chain_size.get() as u64, ALIGNMENT as u64);
|
||||
|
||||
reader.seek(io::SeekFrom::Start(offset))?;
|
||||
let raw_ticket: Box<[u8]> = read_box_slice(reader, header.ticket_size.get() as usize)?;
|
||||
offset = align_up(offset + header.ticket_size.get() as u64, ALIGNMENT as u64);
|
||||
|
||||
reader.seek(io::SeekFrom::Start(offset))?;
|
||||
let raw_tmd: Box<[u8]> = read_box_slice(reader, header.tmd_size.get() as usize)?;
|
||||
offset = align_up(offset + header.tmd_size.get() as u64, ALIGNMENT as u64);
|
||||
|
||||
let content_offset = offset;
|
||||
let mut file = WadFile {
|
||||
header,
|
||||
title_key: [0; 16],
|
||||
fake_signed: false,
|
||||
raw_cert_chain,
|
||||
raw_ticket,
|
||||
raw_tmd,
|
||||
content_offset,
|
||||
};
|
||||
|
||||
let mut title_key_found = false;
|
||||
if file.ticket().header.sig.iter().all(|&x| x == 0) {
|
||||
// Fake signed, try to determine common key index
|
||||
file.fake_signed = true;
|
||||
let contents = file.contents();
|
||||
if let Some(smallest_content) = contents.iter().min_by_key(|x| x.size.get()) {
|
||||
let mut ticket = file.ticket().clone();
|
||||
for i in 0..2 {
|
||||
ticket.common_key_idx = i;
|
||||
let title_key = ticket.decrypt_title_key()?;
|
||||
let offset = file.content_offset(smallest_content.content_index.get());
|
||||
reader.seek(io::SeekFrom::Start(offset))?;
|
||||
if verify_content(reader, smallest_content, &title_key)? {
|
||||
file.title_key = title_key;
|
||||
title_key_found = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
if !title_key_found {
|
||||
bail!("Failed to determine title key for fake signed WAD");
|
||||
}
|
||||
}
|
||||
if !title_key_found {
|
||||
let title_key = file.ticket().decrypt_title_key()?;
|
||||
file.title_key = title_key;
|
||||
}
|
||||
|
||||
Ok(file)
|
||||
}
|
||||
|
||||
pub fn verify_wad<R>(file: &WadFile, reader: &mut R) -> Result<()>
|
||||
where R: Read + Seek + ?Sized {
|
||||
for content in file.contents() {
|
||||
let content_index = content.content_index.get();
|
||||
println!(
|
||||
"Verifying content {:08x} (size {})",
|
||||
content_index,
|
||||
Size::from_bytes(content.size.get())
|
||||
);
|
||||
let offset = file.content_offset(content_index);
|
||||
reader.seek(io::SeekFrom::Start(offset))?;
|
||||
if !verify_content(reader, content, &file.title_key)? {
|
||||
bail!("Content {:08x} hash mismatch", content_index);
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn verify_content<R>(
|
||||
reader: &mut R,
|
||||
content: &ContentMetadata,
|
||||
title_key: &KeyBytes,
|
||||
) -> Result<bool>
|
||||
where
|
||||
R: Read + ?Sized,
|
||||
{
|
||||
let mut buf = <[[u8; 0x10]]>::new_box_zeroed_with_elems(0x200)
|
||||
.map_err(|_| io::Error::from(io::ErrorKind::OutOfMemory))?;
|
||||
// Read full padded size for decryption
|
||||
let read_size = align_up(content.size.get(), 0x40);
|
||||
let mut decryptor = Aes128Cbc::new(title_key.into(), (&content.iv()).into());
|
||||
let mut digest = Sha1::default();
|
||||
let mut read = 0;
|
||||
while read < read_size {
|
||||
let len = buf.len().min(usize::try_from(read_size - read).unwrap_or(usize::MAX));
|
||||
debug_assert_eq!(len % 0x10, 0);
|
||||
reader.read_exact(&mut buf.as_mut_bytes()[..len])?;
|
||||
for block in buf.iter_mut().take(len / 0x10) {
|
||||
decryptor.decrypt_block_mut(block.into());
|
||||
}
|
||||
// Only hash up to content size
|
||||
let hash_len = (read + len as u64).min(content.size.get()).saturating_sub(read) as usize;
|
||||
if hash_len > 0 {
|
||||
digest.update(&buf.as_bytes()[..hash_len]);
|
||||
}
|
||||
read += len as u64;
|
||||
}
|
||||
Ok(HashBytes::from(digest.finalize()) == content.hash)
|
||||
}
|
@ -112,7 +112,7 @@ impl Seek for WindowedFile {
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn stream_position(&mut self) -> io::Result<u64> { Ok(self.pos - self.begin) }
|
||||
fn stream_position(&mut self) -> io::Result<u64> { Ok(self.pos) }
|
||||
}
|
||||
|
||||
impl VfsFile for WindowedFile {
|
||||
|
@ -10,7 +10,7 @@ use nodtool::{
|
||||
nod::{Disc, DiscStream, Fst, NodeKind, OwnedFileStream, PartitionBase, PartitionMeta},
|
||||
};
|
||||
use typed_path::Utf8UnixPath;
|
||||
use zerocopy::{FromZeros, IntoBytes};
|
||||
use zerocopy::IntoBytes;
|
||||
|
||||
use super::{
|
||||
next_non_empty, StaticFile, Vfs, VfsError, VfsFile, VfsFileType, VfsMetadata, VfsResult,
|
||||
@ -252,21 +252,19 @@ impl DiscFile {
|
||||
Self { inner: DiscFileInner::Stream(file), mtime }
|
||||
}
|
||||
|
||||
fn convert_to_mapped(&mut self) -> io::Result<()> {
|
||||
fn convert_to_mapped(&mut self) {
|
||||
match &mut self.inner {
|
||||
DiscFileInner::Stream(stream) => {
|
||||
let pos = stream.stream_position()?;
|
||||
stream.seek(SeekFrom::Start(0))?;
|
||||
let mut data = <[u8]>::new_box_zeroed_with_elems(stream.len() as usize)
|
||||
.map_err(|_| io::Error::from(io::ErrorKind::OutOfMemory))?;
|
||||
stream.read_exact(&mut data)?;
|
||||
let mut cursor = Cursor::new(Arc::from(data));
|
||||
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(_) => {}
|
||||
};
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
@ -306,7 +304,7 @@ impl Seek for DiscFile {
|
||||
|
||||
impl VfsFile for DiscFile {
|
||||
fn map(&mut self) -> io::Result<&[u8]> {
|
||||
self.convert_to_mapped()?;
|
||||
self.convert_to_mapped();
|
||||
match &mut self.inner {
|
||||
DiscFileInner::Stream(_) => unreachable!(),
|
||||
DiscFileInner::Mapped(data) => Ok(data.get_ref()),
|
||||
@ -333,7 +331,7 @@ impl VfsFile for DiscFile {
|
||||
|
||||
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}")),
|
||||
nod::Error::Io(msg, e) => io::Error::new(e.kind(), format!("{}: {}", msg, e)),
|
||||
e => io::Error::new(io::ErrorKind::InvalidData, e),
|
||||
}
|
||||
}
|
||||
|
@ -3,7 +3,6 @@ mod disc;
|
||||
mod rarc;
|
||||
mod std_fs;
|
||||
mod u8_arc;
|
||||
mod wad;
|
||||
|
||||
use std::{
|
||||
error::Error,
|
||||
@ -23,14 +22,12 @@ use rarc::RarcFs;
|
||||
pub use std_fs::StdFs;
|
||||
use typed_path::{Utf8NativePath, Utf8UnixPath, Utf8UnixPathBuf};
|
||||
use u8_arc::U8Fs;
|
||||
use wad::WadFs;
|
||||
|
||||
use crate::util::{
|
||||
ncompress::{YAY0_MAGIC, YAZ0_MAGIC},
|
||||
nlzss,
|
||||
rarc::RARC_MAGIC,
|
||||
u8_arc::U8_MAGIC,
|
||||
wad::WAD_MAGIC,
|
||||
};
|
||||
|
||||
pub trait Vfs: DynClone + Send + Sync {
|
||||
@ -108,8 +105,8 @@ 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::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"),
|
||||
}
|
||||
@ -129,8 +126,8 @@ 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}"),
|
||||
FileFormat::Compressed(kind) => write!(f, "Compressed: {}", kind),
|
||||
FileFormat::Archive(kind) => write!(f, "Archive: {}", kind),
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -157,7 +154,6 @@ pub enum ArchiveKind {
|
||||
Rarc,
|
||||
U8,
|
||||
Disc(nod::Format),
|
||||
Wad,
|
||||
}
|
||||
|
||||
impl Display for ArchiveKind {
|
||||
@ -165,8 +161,7 @@ impl Display for ArchiveKind {
|
||||
match self {
|
||||
ArchiveKind::Rarc => write!(f, "RARC"),
|
||||
ArchiveKind::U8 => write!(f, "U8"),
|
||||
ArchiveKind::Disc(format) => write!(f, "Disc ({format})"),
|
||||
ArchiveKind::Wad => write!(f, "WAD"),
|
||||
ArchiveKind::Disc(format) => write!(f, "Disc ({})", format),
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -174,19 +169,18 @@ impl Display for ArchiveKind {
|
||||
pub fn detect<R>(file: &mut R) -> io::Result<FileFormat>
|
||||
where R: Read + Seek + ?Sized {
|
||||
file.seek(SeekFrom::Start(0))?;
|
||||
let mut magic = [0u8; 8];
|
||||
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(-8)?;
|
||||
file.seek_relative(-4)?;
|
||||
match magic {
|
||||
_ if magic.starts_with(&YAY0_MAGIC) => Ok(FileFormat::Compressed(CompressionKind::Yay0)),
|
||||
_ if magic.starts_with(&YAZ0_MAGIC) => Ok(FileFormat::Compressed(CompressionKind::Yaz0)),
|
||||
_ if magic.starts_with(&RARC_MAGIC) => Ok(FileFormat::Archive(ArchiveKind::Rarc)),
|
||||
_ if magic.starts_with(&U8_MAGIC) => Ok(FileFormat::Archive(ArchiveKind::U8)),
|
||||
WAD_MAGIC => Ok(FileFormat::Archive(ArchiveKind::Wad)),
|
||||
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))?;
|
||||
@ -228,13 +222,13 @@ pub fn open_path_with_fs(
|
||||
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}")),
|
||||
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}"))?,
|
||||
.with_context(|| format!("Failed to open {}", current_path))?,
|
||||
);
|
||||
}
|
||||
VfsFileType::Directory => {
|
||||
@ -248,7 +242,7 @@ pub fn open_path_with_fs(
|
||||
}
|
||||
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}"))?;
|
||||
.with_context(|| format!("Failed to detect file type for {}", current_path))?;
|
||||
if let Some(&next) = split.peek() {
|
||||
match next {
|
||||
"nlzss" => {
|
||||
@ -256,7 +250,7 @@ pub fn open_path_with_fs(
|
||||
file = Some(
|
||||
decompress_file(current_file.as_mut(), CompressionKind::Nlzss)
|
||||
.with_context(|| {
|
||||
format!("Failed to decompress {current_path} with NLZSS")
|
||||
format!("Failed to decompress {} with NLZSS", current_path)
|
||||
})?,
|
||||
);
|
||||
}
|
||||
@ -265,7 +259,7 @@ pub fn open_path_with_fs(
|
||||
file = Some(
|
||||
decompress_file(current_file.as_mut(), CompressionKind::Yay0)
|
||||
.with_context(|| {
|
||||
format!("Failed to decompress {current_path} with Yay0")
|
||||
format!("Failed to decompress {} with Yay0", current_path)
|
||||
})?,
|
||||
);
|
||||
}
|
||||
@ -274,7 +268,7 @@ pub fn open_path_with_fs(
|
||||
file = Some(
|
||||
decompress_file(current_file.as_mut(), CompressionKind::Yaz0)
|
||||
.with_context(|| {
|
||||
format!("Failed to decompress {current_path} with Yaz0")
|
||||
format!("Failed to decompress {} with Yaz0", current_path)
|
||||
})?,
|
||||
);
|
||||
}
|
||||
@ -283,15 +277,16 @@ pub fn open_path_with_fs(
|
||||
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}"))?,
|
||||
);
|
||||
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}"))?;
|
||||
fs = open_fs(current_file, kind).with_context(|| {
|
||||
format!("Failed to open container {}", current_path)
|
||||
})?;
|
||||
// Continue the loop to open the next segment
|
||||
}
|
||||
},
|
||||
@ -301,7 +296,7 @@ pub fn open_path_with_fs(
|
||||
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}"))?,
|
||||
.with_context(|| format!("Failed to decompress {}", current_path))?,
|
||||
segment.to_path_buf(),
|
||||
)),
|
||||
_ => Ok(OpenResult::File(current_file, segment.to_path_buf())),
|
||||
@ -337,7 +332,6 @@ pub fn open_fs(mut file: Box<dyn VfsFile>, kind: ArchiveKind) -> io::Result<Box<
|
||||
disc.open_partition_kind(nod::PartitionKind::Data).map_err(nod_to_io_error)?;
|
||||
Ok(Box::new(DiscFs::new(disc, partition, metadata.mtime)?))
|
||||
}
|
||||
ArchiveKind::Wad => Ok(Box::new(WadFs::new(file)?)),
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -55,10 +55,10 @@ impl StdFile {
|
||||
pub fn new(path: Utf8NativePathBuf) -> Self { StdFile { path, file: None, mmap: None } }
|
||||
|
||||
pub fn file(&mut self) -> io::Result<&mut BufReader<fs::File>> {
|
||||
Ok(match self.file {
|
||||
Some(ref mut file) => file,
|
||||
None => self.file.insert(BufReader::new(fs::File::open(&self.path)?)),
|
||||
})
|
||||
if self.file.is_none() {
|
||||
self.file = Some(BufReader::new(fs::File::open(&self.path)?));
|
||||
}
|
||||
Ok(self.file.as_mut().unwrap())
|
||||
}
|
||||
}
|
||||
|
||||
@ -86,15 +86,13 @@ impl Seek for StdFile {
|
||||
|
||||
impl VfsFile for StdFile {
|
||||
fn map(&mut self) -> io::Result<&[u8]> {
|
||||
let file = match self.file {
|
||||
Some(ref mut file) => file,
|
||||
None => self.file.insert(BufReader::new(fs::File::open(&self.path)?)),
|
||||
};
|
||||
let mmap = match self.mmap {
|
||||
Some(ref mmap) => mmap,
|
||||
None => self.mmap.insert(unsafe { memmap2::Mmap::map(file.get_ref())? }),
|
||||
};
|
||||
Ok(mmap)
|
||||
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> {
|
||||
|
366
src/vfs/wad.rs
366
src/vfs/wad.rs
@ -1,366 +0,0 @@
|
||||
use std::{
|
||||
io,
|
||||
io::{BufRead, Cursor, Read, Seek, SeekFrom},
|
||||
sync::Arc,
|
||||
};
|
||||
|
||||
use aes::cipher::{BlockDecryptMut, KeyIvInit};
|
||||
use filetime::FileTime;
|
||||
use nodtool::nod::DiscStream;
|
||||
use typed_path::Utf8UnixPath;
|
||||
use zerocopy::FromZeros;
|
||||
|
||||
use crate::{
|
||||
array_ref,
|
||||
util::wad::{align_up, process_wad, ContentMetadata, WadFile},
|
||||
vfs::{
|
||||
common::{StaticFile, WindowedFile},
|
||||
Vfs, VfsError, VfsFile, VfsFileType, VfsMetadata, VfsResult,
|
||||
},
|
||||
};
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct WadFs {
|
||||
file: Box<dyn VfsFile>,
|
||||
wad: WadFile,
|
||||
mtime: Option<FileTime>,
|
||||
}
|
||||
|
||||
enum WadFindResult<'a> {
|
||||
Root,
|
||||
Static(&'a [u8]),
|
||||
Content(u16, &'a ContentMetadata),
|
||||
Window(u64, u64),
|
||||
}
|
||||
|
||||
impl WadFs {
|
||||
pub fn new(mut file: Box<dyn VfsFile>) -> io::Result<Self> {
|
||||
let mtime = file.metadata()?.mtime;
|
||||
let wad = process_wad(file.as_mut())
|
||||
.map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e))?;
|
||||
Ok(Self { file, wad, mtime })
|
||||
}
|
||||
|
||||
fn find(&self, path: &str) -> Option<WadFindResult> {
|
||||
let filename = path.trim_start_matches('/');
|
||||
if filename.contains('/') {
|
||||
return None;
|
||||
}
|
||||
if filename.is_empty() {
|
||||
return Some(WadFindResult::Root);
|
||||
}
|
||||
let filename = filename.to_ascii_lowercase();
|
||||
if let Some(id) = filename.strip_suffix(".app") {
|
||||
if let Ok(content_index) = u16::from_str_radix(id, 16) {
|
||||
if let Some(content) = self.wad.contents().get(content_index as usize) {
|
||||
return Some(WadFindResult::Content(content_index, content));
|
||||
}
|
||||
}
|
||||
return None;
|
||||
}
|
||||
let title_id = hex::encode(self.wad.ticket().title_id);
|
||||
match filename.strip_prefix(&title_id) {
|
||||
Some(".tik") => Some(WadFindResult::Static(&self.wad.raw_ticket)),
|
||||
Some(".tmd") => Some(WadFindResult::Static(&self.wad.raw_tmd)),
|
||||
Some(".cert") => Some(WadFindResult::Static(&self.wad.raw_cert_chain)),
|
||||
Some(".trailer") => {
|
||||
if self.wad.header.footer_size.get() == 0 {
|
||||
return None;
|
||||
}
|
||||
Some(WadFindResult::Window(
|
||||
self.wad.trailer_offset(),
|
||||
self.wad.header.footer_size.get() as u64,
|
||||
))
|
||||
}
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Vfs for WadFs {
|
||||
fn open(&mut self, path: &Utf8UnixPath) -> VfsResult<Box<dyn VfsFile>> {
|
||||
if let Some(result) = self.find(path.as_str()) {
|
||||
match result {
|
||||
WadFindResult::Root => Err(VfsError::IsADirectory),
|
||||
WadFindResult::Static(data) => {
|
||||
Ok(Box::new(StaticFile::new(Arc::from(data), self.mtime)))
|
||||
}
|
||||
WadFindResult::Content(content_index, content) => {
|
||||
let offset = self.wad.content_offset(content_index);
|
||||
Ok(Box::new(WadContent::new(
|
||||
AesCbcStream::new(
|
||||
self.file.clone(),
|
||||
offset,
|
||||
content.size.get(),
|
||||
&self.wad.title_key,
|
||||
&content.iv(),
|
||||
),
|
||||
self.mtime,
|
||||
)))
|
||||
}
|
||||
WadFindResult::Window(offset, len) => {
|
||||
Ok(Box::new(WindowedFile::new(self.file.clone(), offset, len)?))
|
||||
}
|
||||
}
|
||||
} else {
|
||||
Err(VfsError::NotFound)
|
||||
}
|
||||
}
|
||||
|
||||
fn exists(&mut self, path: &Utf8UnixPath) -> VfsResult<bool> {
|
||||
Ok(self.find(path.as_str()).is_some())
|
||||
}
|
||||
|
||||
fn read_dir(&mut self, path: &Utf8UnixPath) -> VfsResult<Vec<String>> {
|
||||
let path = path.as_str().trim_start_matches('/');
|
||||
if !path.is_empty() {
|
||||
return Err(VfsError::NotFound);
|
||||
}
|
||||
let title_id = hex::encode(self.wad.ticket().title_id);
|
||||
let mut entries = Vec::new();
|
||||
entries.push(format!("{title_id}.tik"));
|
||||
entries.push(format!("{title_id}.tmd"));
|
||||
entries.push(format!("{title_id}.cert"));
|
||||
if self.wad.header.footer_size.get() > 0 {
|
||||
entries.push(format!("{title_id}.trailer"));
|
||||
}
|
||||
for content in self.wad.contents() {
|
||||
entries.push(format!("{:08x}.app", content.content_index.get()));
|
||||
}
|
||||
Ok(entries)
|
||||
}
|
||||
|
||||
fn metadata(&mut self, path: &Utf8UnixPath) -> VfsResult<VfsMetadata> {
|
||||
if let Some(result) = self.find(path.as_str()) {
|
||||
match result {
|
||||
WadFindResult::Root => {
|
||||
Ok(VfsMetadata { file_type: VfsFileType::Directory, len: 0, mtime: self.mtime })
|
||||
}
|
||||
WadFindResult::Static(data) => Ok(VfsMetadata {
|
||||
file_type: VfsFileType::File,
|
||||
len: data.len() as u64,
|
||||
mtime: self.mtime,
|
||||
}),
|
||||
WadFindResult::Content(_, content) => Ok(VfsMetadata {
|
||||
file_type: VfsFileType::File,
|
||||
len: content.size.get(),
|
||||
mtime: self.mtime,
|
||||
}),
|
||||
WadFindResult::Window(_, len) => {
|
||||
Ok(VfsMetadata { file_type: VfsFileType::File, len, mtime: self.mtime })
|
||||
}
|
||||
}
|
||||
} else {
|
||||
Err(VfsError::NotFound)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
enum WadContentInner {
|
||||
Stream(AesCbcStream),
|
||||
Mapped(Cursor<Arc<[u8]>>),
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
struct WadContent {
|
||||
inner: WadContentInner,
|
||||
mtime: Option<FileTime>,
|
||||
}
|
||||
|
||||
impl WadContent {
|
||||
fn new(inner: AesCbcStream, mtime: Option<FileTime>) -> Self {
|
||||
Self { inner: WadContentInner::Stream(inner), mtime }
|
||||
}
|
||||
|
||||
fn convert_to_mapped(&mut self) -> io::Result<()> {
|
||||
match &mut self.inner {
|
||||
WadContentInner::Stream(stream) => {
|
||||
let pos = stream.stream_position()?;
|
||||
stream.seek(SeekFrom::Start(0))?;
|
||||
let mut data = <[u8]>::new_box_zeroed_with_elems(stream.len() as usize)
|
||||
.map_err(|_| io::Error::from(io::ErrorKind::OutOfMemory))?;
|
||||
stream.read_exact(&mut data)?;
|
||||
let mut cursor = Cursor::new(Arc::from(data));
|
||||
cursor.set_position(pos);
|
||||
self.inner = WadContentInner::Mapped(cursor);
|
||||
}
|
||||
WadContentInner::Mapped(_) => {}
|
||||
};
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl BufRead for WadContent {
|
||||
fn fill_buf(&mut self) -> io::Result<&[u8]> {
|
||||
match &mut self.inner {
|
||||
WadContentInner::Stream(stream) => stream.fill_buf(),
|
||||
WadContentInner::Mapped(data) => data.fill_buf(),
|
||||
}
|
||||
}
|
||||
|
||||
fn consume(&mut self, amt: usize) {
|
||||
match &mut self.inner {
|
||||
WadContentInner::Stream(stream) => stream.consume(amt),
|
||||
WadContentInner::Mapped(data) => data.consume(amt),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Read for WadContent {
|
||||
fn read(&mut self, buf: &mut [u8]) -> io::Result<usize> {
|
||||
match &mut self.inner {
|
||||
WadContentInner::Stream(stream) => stream.read(buf),
|
||||
WadContentInner::Mapped(data) => data.read(buf),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Seek for WadContent {
|
||||
fn seek(&mut self, pos: SeekFrom) -> io::Result<u64> {
|
||||
match &mut self.inner {
|
||||
WadContentInner::Stream(stream) => stream.seek(pos),
|
||||
WadContentInner::Mapped(data) => data.seek(pos),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl VfsFile for WadContent {
|
||||
fn map(&mut self) -> io::Result<&[u8]> {
|
||||
self.convert_to_mapped()?;
|
||||
match &mut self.inner {
|
||||
WadContentInner::Stream(_) => unreachable!(),
|
||||
WadContentInner::Mapped(data) => Ok(data.get_ref()),
|
||||
}
|
||||
}
|
||||
|
||||
fn metadata(&mut self) -> io::Result<VfsMetadata> {
|
||||
match &mut self.inner {
|
||||
WadContentInner::Stream(stream) => Ok(VfsMetadata {
|
||||
file_type: VfsFileType::File,
|
||||
len: stream.len(),
|
||||
mtime: self.mtime,
|
||||
}),
|
||||
WadContentInner::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 }
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
struct AesCbcStream {
|
||||
inner: Box<dyn VfsFile>,
|
||||
position: u64,
|
||||
content_offset: u64,
|
||||
content_size: u64,
|
||||
key: [u8; 0x10],
|
||||
init_iv: [u8; 0x10],
|
||||
last_iv: [u8; 0x10],
|
||||
block_idx: u64,
|
||||
block: Box<[u8; 0x200]>,
|
||||
}
|
||||
|
||||
impl AesCbcStream {
|
||||
fn new(
|
||||
inner: Box<dyn VfsFile>,
|
||||
content_offset: u64,
|
||||
content_size: u64,
|
||||
key: &[u8; 0x10],
|
||||
iv: &[u8; 0x10],
|
||||
) -> Self {
|
||||
let block = <[u8; 0x200]>::new_box_zeroed().unwrap();
|
||||
Self {
|
||||
inner,
|
||||
position: 0,
|
||||
content_offset,
|
||||
content_size,
|
||||
key: *key,
|
||||
init_iv: *iv,
|
||||
last_iv: [0u8; 0x10],
|
||||
block_idx: u64::MAX,
|
||||
block,
|
||||
}
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn len(&self) -> u64 { self.content_size }
|
||||
|
||||
#[inline]
|
||||
fn remaining(&self) -> u64 { self.content_size.saturating_sub(self.position) }
|
||||
}
|
||||
|
||||
impl Read for AesCbcStream {
|
||||
fn read(&mut self, mut buf: &mut [u8]) -> io::Result<usize> {
|
||||
let mut total = 0;
|
||||
while !buf.is_empty() {
|
||||
let block = self.fill_buf()?;
|
||||
if block.is_empty() {
|
||||
break;
|
||||
}
|
||||
let len = buf.len().min(block.len());
|
||||
buf[..len].copy_from_slice(&block[..len]);
|
||||
buf = &mut buf[len..];
|
||||
self.consume(len);
|
||||
total += len;
|
||||
}
|
||||
Ok(total)
|
||||
}
|
||||
}
|
||||
|
||||
impl BufRead for AesCbcStream {
|
||||
fn fill_buf(&mut self) -> io::Result<&[u8]> {
|
||||
if self.position >= self.content_size {
|
||||
return Ok(&[]);
|
||||
}
|
||||
let block_size = self.block.len();
|
||||
let current_block = self.position / block_size as u64;
|
||||
if current_block != self.block_idx {
|
||||
let block_offset = current_block * block_size as u64;
|
||||
let mut iv = [0u8; 0x10];
|
||||
if current_block == 0 {
|
||||
// Use the initial IV for the first block
|
||||
self.inner.seek(SeekFrom::Start(self.content_offset))?;
|
||||
iv = self.init_iv;
|
||||
} else if self.block_idx.checked_add(1) == Some(current_block) {
|
||||
// Shortcut to avoid seeking when reading sequentially
|
||||
iv = self.last_iv;
|
||||
} else {
|
||||
// Read the IV from the previous block
|
||||
self.inner.seek(SeekFrom::Start(self.content_offset + block_offset - 0x10))?;
|
||||
self.inner.read_exact(&mut iv)?;
|
||||
}
|
||||
let aligned_size = align_up(self.content_size, 0x10);
|
||||
let remaining = aligned_size.saturating_sub(block_offset);
|
||||
let read = remaining.min(block_size as u64) as usize;
|
||||
self.inner.read_exact(&mut self.block[..read])?;
|
||||
self.last_iv = *array_ref!(self.block, read - 0x10, 0x10);
|
||||
let mut decryptor =
|
||||
cbc::Decryptor::<aes::Aes128>::new((&self.key).into(), (&iv).into());
|
||||
for aes_block in self.block[..read].chunks_exact_mut(0x10) {
|
||||
decryptor.decrypt_block_mut(aes_block.into());
|
||||
}
|
||||
self.block_idx = current_block;
|
||||
}
|
||||
let offset = (self.position % block_size as u64) as usize;
|
||||
let len = self.remaining().min((block_size - offset) as u64) as usize;
|
||||
Ok(&self.block[offset..offset + len])
|
||||
}
|
||||
|
||||
fn consume(&mut self, amt: usize) { self.position = self.position.saturating_add(amt as u64); }
|
||||
}
|
||||
|
||||
impl Seek for AesCbcStream {
|
||||
fn seek(&mut self, pos: SeekFrom) -> io::Result<u64> {
|
||||
self.position = match pos {
|
||||
SeekFrom::Start(p) => p,
|
||||
SeekFrom::End(p) => self.content_size.saturating_add_signed(p),
|
||||
SeekFrom::Current(p) => self.position.saturating_add_signed(p),
|
||||
};
|
||||
Ok(self.position)
|
||||
}
|
||||
}
|
Loading…
x
Reference in New Issue
Block a user