Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
59 changes: 57 additions & 2 deletions src/game_engine/unity/il2cpp/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -343,7 +343,8 @@ impl Module {
/// UTF-16 units, a nul character among them like any other. `N` bounds
/// how many units the string holds, and a string claiming more than that
/// fails rather than truncates, as do a negative count and a null
/// reference.
/// reference. A string already held by its object address reads with
/// [`read_string_object`](Self::read_string_object).
pub fn read_string<const N: usize>(
&self,
process: &Process,
Expand All @@ -352,14 +353,32 @@ impl Module {
managed::read_string(process, self.pointer_size, at)
}

/// Reads a managed string at its object address rather than through a
/// reference: the form for an element
/// [`read_reference_array`](Self::read_reference_array) hands back. A
/// null address fails, so a null element refuses at the element with
/// its index in hand. The returned [`ManagedString`] holds exactly the
/// string's count of UTF-16 units, a nul character among them like any
/// other. `N` bounds how many units the string holds, and a string
/// claiming more than that fails rather than truncates, as does a
/// negative count.
pub fn read_string_object<const N: usize>(
&self,
process: &Process,
object: Address,
) -> Result<ManagedString<N>, Error> {
managed::read_string_object(process, self.pointer_size, object)
}

/// Reads a managed array of value elements through the reference stored
/// at the given address. The array carries its own length, so no count
/// is passed; `N` bounds how many elements the returned [`ArrayVec`]
/// holds, and an array claiming more than that fails rather than
/// truncates, as does a null reference. The element type is the caller's
/// claim and has to match the target's own element layout: a managed
/// `char` is a `u16` here, a `bool` a single byte, and Rust's `char` and
/// `usize` never match. Reference elements have no portable claim.
/// `usize` never match. Reference elements read with
/// [`read_reference_array`](Self::read_reference_array).
pub fn read_array<T: CheckedBitPattern, const N: usize>(
&self,
process: &Process,
Expand All @@ -368,6 +387,24 @@ impl Module {
managed::read_array(process, self.pointer_size, at)
}

/// Reads a managed array of reference elements through the reference
/// stored at the given address, handing back the elements' object
/// addresses read at the target's own pointer width. A null array
/// reference fails; null elements are data and come back as null
/// addresses at their positions, since positions carry meaning and
/// filtering is the caller's choice. `N` bounds how many elements the
/// returned vector holds, and an array claiming more than that fails
/// rather than truncates. A `string` element reads with
/// [`read_string_object`](Self::read_string_object); any other object's
/// fields read at its address directly.
pub fn read_reference_array<const N: usize>(
&self,
process: &Process,
at: Address,
) -> Result<ArrayVec<Address, N>, Error> {
managed::read_reference_array(process, self.pointer_size, at)
}

/// Resolves where a `List` keeps its backing array and live count, finding
/// `System.Collections.Generic.List` in the class hierarchy of the object
/// at the given address. The answer is a small `Copy` value worth storing,
Expand Down Expand Up @@ -490,6 +527,24 @@ impl Module {
managed::read_list(process, self.pointer_size, offsets, at)
}

/// Reads a managed `List` of reference elements through the reference
/// stored at the given address, with the offsets
/// [`get_list_offsets`](Self::get_list_offsets) resolved, handing back
/// the elements' object addresses read at the target's own pointer
/// width. The list's live count is read, never its backing capacity;
/// `N` bounds the count, and a count past the buffer or past the
/// backing array's own length fails rather than truncates, as does a
/// null list reference. Null elements are data and come back as null
/// addresses at their positions.
pub fn read_reference_list<const N: usize>(
&self,
process: &Process,
offsets: ListOffsets,
at: Address,
) -> Result<ArrayVec<Address, N>, Error> {
managed::read_reference_list(process, self.pointer_size, offsets, at)
}

/// Attaches to a Unity game that is using the IL2CPP backend. This function
/// automatically detects the [IL2CPP version](Version). If you know the
/// version in advance or it fails detecting it, use
Expand Down
22 changes: 22 additions & 0 deletions src/game_engine/unity/il2cpp/readers_tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,11 @@ fn image() -> Vec<u8> {
put(&mut i, 0x300 + 0x20, &11_u32.to_le_bytes());
put(&mut i, 0x300 + 0x24, &22_u32.to_le_bytes());

// A string array holding the string object and a null element.
ptr(&mut i, 0x18, BASE + 0x400);
put(&mut i, 0x400 + 0x18, &2_u64.to_le_bytes());
ptr(&mut i, 0x400 + 0x20, BASE + 0x100);

i
}

Expand Down Expand Up @@ -85,3 +90,20 @@ fn array_lengths_judge_at_full_width() {
.is_err());
});
}

#[test]
fn reference_arrays_resolve_through_their_reference() {
on_fixture(|process, module| {
let objects = module
.read_reference_array::<4>(process, Address::new(BASE + 0x18))
.unwrap();
assert_eq!(
objects.as_slice(),
[Address::new(BASE + 0x100), Address::NULL]
);

let read = module.read_string_object::<8>(process, objects[0]).unwrap();
assert!(read.matches_str("Loop"));
assert!(module.read_string_object::<8>(process, objects[1]).is_err());
});
}
26 changes: 26 additions & 0 deletions src/game_engine/unity/il2cpp/walk_tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -647,6 +647,15 @@ fn image(version: Version) -> Vec<u8> {
ptr(&mut i, 0x3800, BASE + lookalike);
ptr(&mut i, 0x28, BASE + 0x3800);

// A reference list shares the class; its backing holds an object
// address and a null element.
ptr(&mut i, 0x1A00, BASE + list_class);
ptr(&mut i, 0x1A00 + 0x10, BASE + 0x1B00);
put(&mut i, 0x1A00 + 0x18, &2_i32.to_le_bytes());
put(&mut i, 0x1B00 + 0x18, &2_u64.to_le_bytes());
ptr(&mut i, 0x1B00 + 0x20, BASE + 0x1080);
ptr(&mut i, 0x30, BASE + 0x1A00);

i
}

Expand Down Expand Up @@ -816,6 +825,23 @@ fn list_shaped_objects_are_not_lists() {
});
}

// A reference list hands back its elements' object addresses, a null
// element preserved at its position.
#[test]
fn reference_lists_resolve_their_element_addresses() {
on_fixture(Version::V2022, |process, module| {
let at = Address::new(BASE + 0x30);
let offsets = module.get_list_offsets(process, at).unwrap();
let read = module
.read_reference_list::<4>(process, offsets, at)
.unwrap();
assert_eq!(
read.as_slice(),
[Address::new(BASE + 0x1080), Address::NULL]
);
});
}

// The whole pointer path: the static root, the instance behind it, and a field
// resolved against the object's own class read off its head.
#[test]
Expand Down
5 changes: 3 additions & 2 deletions src/game_engine/unity/managed/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,9 @@ mod walk;
pub use cursor::{Assemblies, Classes};
pub use pointer::PointerPath;
pub use readers::{
read_array, read_dictionary, read_hash_set, read_list, read_string, DictionaryOffsets,
EntryLayout, HashSetOffsets, ListOffsets, SlotLayout,
read_array, read_dictionary, read_hash_set, read_list, read_reference_array,
read_reference_list, read_string, read_string_object, DictionaryOffsets, EntryLayout,
HashSetOffsets, ListOffsets, SlotLayout,
};
#[cfg(test)]
pub(crate) use readers::{DictionaryShape, SetShape};
Expand Down
78 changes: 73 additions & 5 deletions src/game_engine/unity/managed/readers.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ use bytemuck::CheckedBitPattern;
use core::mem::{size_of, MaybeUninit};

use super::ManagedString;
use crate::{Address, Error, PointerSize, Process};
use crate::{Address, Address32, Address64, Error, PointerSize, Process};

/// How many bytes a managed object's two header words occupy. The readers
/// skip them; nothing in them is read.
Expand All @@ -12,10 +12,6 @@ const fn object_header(pointer_size: PointerSize) -> u64 {
}

/// Reads a managed string through the reference stored at the given address.
/// The layout is runtime ABI, shared by both runtimes at both widths: the
/// character count as an i32 past the object header, the UTF-16 characters
/// inline behind it. The count is the string's length, so a nul character
/// inside the string is kept like any other.
pub fn read_string<const N: usize>(
process: &Process,
pointer_size: PointerSize,
Expand All @@ -27,6 +23,23 @@ pub fn read_string<const N: usize>(
.filter(|address| !address.is_null())
.ok_or(Error {})?;

read_string_object(process, pointer_size, object)
}

/// Reads a managed string at its object address, the form a reference
/// array's elements hand back. The layout is runtime ABI, shared by both
/// runtimes at both widths: the character count as an i32 past the object
/// header, the UTF-16 characters inline behind it. The count is the string's
/// length, so a nul character inside the string is kept like any other.
pub fn read_string_object<const N: usize>(
process: &Process,
pointer_size: PointerSize,
object: Address,
) -> Result<ManagedString<N>, Error> {
if object.is_null() {
return Err(Error {});
}

let header = object_header(pointer_size);
let count = process.read::<i32>(object + header)?;

Expand Down Expand Up @@ -582,3 +595,58 @@ pub fn read_array<T: CheckedBitPattern, const N: usize>(
.map_err(|_| Error {})?;
Ok(array)
}

/// Reads a managed array of reference elements through the reference
/// stored at the given address, handing back the elements' object
/// addresses. The element slots are pointers at the target's own width,
/// which the reader owns rather than taking as a claim. Null elements
/// are data and stay at their positions; a null array reference fails.
pub fn read_reference_array<const N: usize>(
process: &Process,
pointer_size: PointerSize,
at: Address,
) -> Result<ArrayVec<Address, N>, Error> {
match pointer_size {
PointerSize::Bit64 => Ok(read_array::<Address64, N>(process, pointer_size, at)?
.iter()
.copied()
.map(Into::into)
.collect()),
PointerSize::Bit32 => Ok(read_array::<Address32, N>(process, pointer_size, at)?
.iter()
.copied()
.map(Into::into)
.collect()),
// No managed runtime is 16-bit.
PointerSize::Bit16 => Err(Error {}),
}
}

/// Reads a managed list of reference elements through the reference
/// stored at the given address, with the offsets a resolution handed out
/// earlier, handing back the elements' object addresses the way
/// [`read_reference_array`] does. The count and backing checks are
/// [`read_list`]'s.
pub fn read_reference_list<const N: usize>(
process: &Process,
pointer_size: PointerSize,
offsets: ListOffsets,
at: Address,
) -> Result<ArrayVec<Address, N>, Error> {
match pointer_size {
PointerSize::Bit64 => Ok(
read_list::<Address64, N>(process, pointer_size, offsets, at)?
.into_iter()
.map(Into::into)
.collect(),
),
PointerSize::Bit32 => Ok(
read_list::<Address32, N>(process, pointer_size, offsets, at)?
.into_iter()
.map(Into::into)
.collect(),
),
// No managed runtime is 16-bit.
PointerSize::Bit16 => Err(Error {}),
}
}
59 changes: 57 additions & 2 deletions src/game_engine/unity/mono/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -347,7 +347,8 @@ impl Module {
/// UTF-16 units, a nul character among them like any other. `N` bounds
/// how many units the string holds, and a string claiming more than that
/// fails rather than truncates, as do a negative count and a null
/// reference.
/// reference. A string already held by its object address reads with
/// [`read_string_object`](Self::read_string_object).
pub fn read_string<const N: usize>(
&self,
process: &Process,
Expand All @@ -356,14 +357,32 @@ impl Module {
managed::read_string(process, self.pointer_size, at)
}

/// Reads a managed string at its object address rather than through a
/// reference: the form for an element
/// [`read_reference_array`](Self::read_reference_array) hands back. A
/// null address fails, so a null element refuses at the element with
/// its index in hand. The returned [`ManagedString`] holds exactly the
/// string's count of UTF-16 units, a nul character among them like any
/// other. `N` bounds how many units the string holds, and a string
/// claiming more than that fails rather than truncates, as does a
/// negative count.
pub fn read_string_object<const N: usize>(
&self,
process: &Process,
object: Address,
) -> Result<ManagedString<N>, Error> {
managed::read_string_object(process, self.pointer_size, object)
}

/// Reads a managed array of value elements through the reference stored
/// at the given address. The array carries its own length, so no count
/// is passed; `N` bounds how many elements the returned [`ArrayVec`]
/// holds, and an array claiming more than that fails rather than
/// truncates, as does a null reference. The element type is the caller's
/// claim and has to match the target's own element layout: a managed
/// `char` is a `u16` here, a `bool` a single byte, and Rust's `char` and
/// `usize` never match. Reference elements have no portable claim.
/// `usize` never match. Reference elements read with
/// [`read_reference_array`](Self::read_reference_array).
pub fn read_array<T: CheckedBitPattern, const N: usize>(
&self,
process: &Process,
Expand All @@ -372,6 +391,24 @@ impl Module {
managed::read_array(process, self.pointer_size, at)
}

/// Reads a managed array of reference elements through the reference
/// stored at the given address, handing back the elements' object
/// addresses read at the target's own pointer width. A null array
/// reference fails; null elements are data and come back as null
/// addresses at their positions, since positions carry meaning and
/// filtering is the caller's choice. `N` bounds how many elements the
/// returned vector holds, and an array claiming more than that fails
/// rather than truncates. A `string` element reads with
/// [`read_string_object`](Self::read_string_object); any other object's
/// fields read at its address directly.
pub fn read_reference_array<const N: usize>(
&self,
process: &Process,
at: Address,
) -> Result<ArrayVec<Address, N>, Error> {
managed::read_reference_array(process, self.pointer_size, at)
}

/// Resolves where a `List` keeps its backing array and live count, finding
/// `System.Collections.Generic.List` in the class hierarchy of the object
/// at the given address. The answer is a small `Copy` value worth storing,
Expand Down Expand Up @@ -494,6 +531,24 @@ impl Module {
managed::read_list(process, self.pointer_size, offsets, at)
}

/// Reads a managed `List` of reference elements through the reference
/// stored at the given address, with the offsets
/// [`get_list_offsets`](Self::get_list_offsets) resolved, handing back
/// the elements' object addresses read at the target's own pointer
/// width. The list's live count is read, never its backing capacity;
/// `N` bounds the count, and a count past the buffer or past the
/// backing array's own length fails rather than truncates, as does a
/// null list reference. Null elements are data and come back as null
/// addresses at their positions.
pub fn read_reference_list<const N: usize>(
&self,
process: &Process,
offsets: ListOffsets,
at: Address,
) -> Result<ArrayVec<Address, N>, Error> {
managed::read_reference_list(process, self.pointer_size, offsets, at)
}

/// Attaches to a Unity game that is using the standard Mono backend. This
/// function automatically detects the [Mono version](Version). If you
/// know the version in advance or it fails detecting it, use
Expand Down
Loading
Loading