mod class_uclass;
mod gnames;
mod guobject;
pub use class_uclass::discover_class_uclass;
pub use gnames::discover_gnames;
pub use guobject::{discover_guobject_array, is_valid_pointer};
use super::source::MemorySource;
use super::ue5::Ue5Offsets;
use anyhow::{bail, Result};
pub fn read_fname(source: &dyn MemorySource, gnames_addr: usize, index: u32) -> Result<String> {
let entry_offset = (index as usize) * 2; let entry_addr = gnames_addr + entry_offset;
if let Ok(data) = source.read_bytes(entry_addr, 128) {
let len_byte = data[0];
let string_len = ((len_byte >> 1) & 0x3F) as usize;
if string_len > 0 && string_len < 64 {
let name_bytes = &data[2..2 + string_len];
if name_bytes
.iter()
.all(|&b| b.is_ascii_graphic() || b == b'_')
{
return Ok(String::from_utf8_lossy(name_bytes).to_string());
}
}
let alt_len = (data[0] >> 6) as usize;
if alt_len > 0 && alt_len < 64 {
let name_bytes = &data[1..1 + alt_len];
if name_bytes
.iter()
.all(|&b| b.is_ascii_graphic() || b == b'_')
{
return Ok(String::from_utf8_lossy(name_bytes).to_string());
}
}
}
bail!("FName index {} not found", index)
}
pub fn find_ue5_offsets(source: &dyn MemorySource) -> Result<Ue5Offsets> {
let gnames = discover_gnames(source)?;
let guobject_array = match discover_guobject_array(source, gnames.address) {
Ok(arr) => arr.address,
Err(_) => 0, };
Ok(Ue5Offsets {
gnames: gnames.address,
guobject_array,
})
}
#[cfg(test)]
mod tests {
use super::*;
use crate::memory::source::tests::MockMemorySource;
use crate::memory::source::MemoryRegion;
use crate::memory::ue5::GNamesPool;
fn create_gnames_mock() -> (Vec<u8>, usize) {
let base = 0x200000000usize;
let mut data = vec![0u8; 4096];
let offset = 100;
data[offset] = 0x1e;
data[offset + 1] = 0x01;
data[offset + 2..offset + 6].copy_from_slice(b"None");
data[offset + 6] = 0x10;
data[offset + 7] = 0x03;
data[offset + 8..offset + 20].copy_from_slice(b"ByteProperty");
(data, base)
}
#[test]
fn test_discover_gnames_finds_pattern() {
let (data, base) = create_gnames_mock();
let source = MockMemorySource::new(data, base);
let result = discover_gnames(&source);
assert!(result.is_ok());
let pool = result.unwrap();
assert_eq!(pool.address, base + 100);
assert!(!pool.sample_names.is_empty());
}
#[test]
fn test_discover_gnames_no_pattern() {
let data = vec![0u8; 4096];
let source = MockMemorySource::new(data, 0x200000000);
let result = discover_gnames(&source);
assert!(result.is_err());
}
#[test]
fn test_discover_gnames_alt_pattern() {
let base = 0x200000000usize;
let mut data = vec![0u8; 4096];
let offset = 100;
data[offset] = 0x08; data[offset + 1] = 0x00;
data[offset + 2..offset + 6].copy_from_slice(b"None");
data[offset + 10..offset + 22].copy_from_slice(b"ByteProperty");
let source = MockMemorySource::new(data, base);
let result = discover_gnames(&source);
assert!(result.is_ok());
}
#[test]
fn test_is_valid_pointer() {
assert!(is_valid_pointer(0x100000));
assert!(is_valid_pointer(0x140000000));
assert!(is_valid_pointer(0x7FFFFFFFFFFF));
assert!(!is_valid_pointer(0));
assert!(!is_valid_pointer(0x1000)); assert!(!is_valid_pointer(0x800000000000)); }
#[test]
fn test_gnames_pool_structure() {
let pool = GNamesPool {
address: 0x200000000,
sample_names: vec![
(0, "None".to_string()),
(1, "ByteProperty".to_string()),
(2, "Class".to_string()),
],
};
assert_eq!(pool.address, 0x200000000);
assert_eq!(pool.sample_names.len(), 3);
assert_eq!(pool.sample_names[0], (0, "None".to_string()));
}
fn create_guobject_code_mock() -> (Vec<u8>, usize) {
let code_base = 0x140001000usize;
let data_base = 0x151000000usize;
let mut code = vec![0u8; 4096];
let pattern_offset = 100;
code[pattern_offset] = 0x48;
code[pattern_offset + 1] = 0x8B;
code[pattern_offset + 2] = 0x05;
let rip = code_base + pattern_offset + 7;
let displacement = (data_base as i64 - rip as i64) as i32;
code[pattern_offset + 3..pattern_offset + 7].copy_from_slice(&displacement.to_le_bytes());
code[pattern_offset + 7..pattern_offset + 15]
.copy_from_slice(&[0x48, 0x8B, 0x0C, 0xC8, 0x48, 0x8D, 0x04, 0xD1]);
(code, code_base)
}
#[test]
fn test_guobject_code_pattern_detection() {
let (code, code_base) = create_guobject_code_mock();
let source = MockMemorySource::with_regions(
code,
code_base,
vec![MemoryRegion {
start: code_base,
end: code_base + 4096,
perms: "r-xp".to_string(),
offset: 0,
path: Some("Borderlands4.exe".to_string()),
}],
);
let data = source.read_bytes(code_base + 100, 15).unwrap();
assert_eq!(data[0], 0x48);
assert_eq!(data[1], 0x8B);
assert_eq!(data[2], 0x05);
assert_eq!(
&data[7..15],
&[0x48, 0x8B, 0x0C, 0xC8, 0x48, 0x8D, 0x04, 0xD1]
);
}
}