use crate::{
emulation::{
runtime::hook::{Hook, HookContext, HookManager, PreHookResult},
thread::EmulationThread,
EmValue,
},
Result,
};
pub fn register(manager: &HookManager) -> Result<()> {
manager.register(
Hook::new("System.Nullable.get_HasValue")
.match_name("System", "Nullable`1", "get_HasValue")
.pre(nullable_has_value_pre),
)?;
manager.register(
Hook::new("System.Nullable.get_Value")
.match_name("System", "Nullable`1", "get_Value")
.pre(nullable_get_value_pre),
)?;
manager.register(
Hook::new("System.Nullable.GetValueOrDefault")
.match_name("System", "Nullable`1", "GetValueOrDefault")
.pre(nullable_get_value_or_default_pre),
)?;
manager.register(
Hook::new("System.Nullable.GetUnderlyingType")
.match_name("System", "Nullable", "GetUnderlyingType")
.pre(nullable_get_underlying_type_pre),
)?;
Ok(())
}
fn nullable_has_value_pre(ctx: &HookContext<'_>, _thread: &mut EmulationThread) -> PreHookResult {
if let Some(this_val) = ctx.this {
let has_value = !matches!(this_val, EmValue::Null);
return PreHookResult::Bypass(Some(EmValue::I32(i32::from(has_value))));
}
PreHookResult::Bypass(Some(EmValue::I32(0)))
}
fn nullable_get_value_pre(ctx: &HookContext<'_>, _thread: &mut EmulationThread) -> PreHookResult {
if let Some(this_val) = ctx.this {
if !matches!(this_val, EmValue::Null) {
return PreHookResult::Bypass(Some(this_val.clone()));
}
}
PreHookResult::throw_invalid_operation("Nullable object must have a value")
}
fn nullable_get_value_or_default_pre(
ctx: &HookContext<'_>,
_thread: &mut EmulationThread,
) -> PreHookResult {
if let Some(this_val) = ctx.this {
if !matches!(this_val, EmValue::Null) {
return PreHookResult::Bypass(Some(this_val.clone()));
}
}
PreHookResult::Bypass(Some(EmValue::I32(0)))
}
fn nullable_get_underlying_type_pre(
ctx: &HookContext<'_>,
thread: &mut EmulationThread,
) -> PreHookResult {
if let Some(EmValue::ObjectRef(type_ref)) = ctx.args.first() {
if let Some(type_token) = try_hook!(thread.heap().get_reflection_type_token(*type_ref)) {
if let Some(asm) = thread.assembly().cloned() {
if let Some(cil_type) = asm.types().get(&type_token) {
if cil_type.name == "Nullable`1" && cil_type.namespace == "System" {
for (_, method_spec) in cil_type.generic_args.iter() {
for (_, type_arg_ref) in method_spec.generic_args.iter() {
let Some(arg_token) = type_arg_ref.token() else {
continue;
};
match thread.heap_mut().alloc_reflection_type(arg_token, None) {
Ok(href) => {
return PreHookResult::Bypass(Some(EmValue::ObjectRef(
href,
)))
}
Err(e) => {
return PreHookResult::Error(format!(
"heap allocation failed: {e}"
))
}
}
}
}
return PreHookResult::Bypass(Some(EmValue::Null));
}
}
}
}
}
PreHookResult::Bypass(Some(EmValue::Null))
}
#[cfg(test)]
mod tests {
use super::*;
use crate::{
emulation::runtime::hook::HookManager,
metadata::{token::Token, typesystem::PointerSize},
test::emulation::create_test_thread,
};
fn ctx<'a>(method: &'a str, this: Option<&'a EmValue>, args: &'a [EmValue]) -> HookContext<'a> {
HookContext::new(
Token::new(0x0A000001),
"System",
"Nullable`1",
method,
PointerSize::Bit64,
)
.with_this(this)
.with_args(args)
}
#[test]
fn test_register_hooks() {
let manager = HookManager::new();
crate::emulation::runtime::bcl::system::nullable::register(&manager).unwrap();
assert_eq!(manager.len(), 4);
}
#[test]
fn test_has_value_true() {
let mut thread = create_test_thread();
let this = EmValue::I32(42);
let result = nullable_has_value_pre(&ctx("get_HasValue", Some(&this), &[]), &mut thread);
assert!(matches!(
result,
PreHookResult::Bypass(Some(EmValue::I32(1)))
));
}
#[test]
fn test_has_value_false() {
let mut thread = create_test_thread();
let this = EmValue::Null;
let result = nullable_has_value_pre(&ctx("get_HasValue", Some(&this), &[]), &mut thread);
assert!(matches!(
result,
PreHookResult::Bypass(Some(EmValue::I32(0)))
));
}
#[test]
fn test_get_value_present() {
let mut thread = create_test_thread();
let this = EmValue::I32(42);
let result = nullable_get_value_pre(&ctx("get_Value", Some(&this), &[]), &mut thread);
assert!(matches!(
result,
PreHookResult::Bypass(Some(EmValue::I32(42)))
));
}
#[test]
fn test_get_value_null_throws() {
let mut thread = create_test_thread();
let this = EmValue::Null;
let result = nullable_get_value_pre(&ctx("get_Value", Some(&this), &[]), &mut thread);
assert!(matches!(result, PreHookResult::Throw { .. }));
}
#[test]
fn test_get_value_or_default_present() {
let mut thread = create_test_thread();
let this = EmValue::I32(42);
let result = nullable_get_value_or_default_pre(
&ctx("GetValueOrDefault", Some(&this), &[]),
&mut thread,
);
assert!(matches!(
result,
PreHookResult::Bypass(Some(EmValue::I32(42)))
));
}
#[test]
fn test_get_value_or_default_null() {
let mut thread = create_test_thread();
let this = EmValue::Null;
let result = nullable_get_value_or_default_pre(
&ctx("GetValueOrDefault", Some(&this), &[]),
&mut thread,
);
assert!(matches!(
result,
PreHookResult::Bypass(Some(EmValue::I32(0)))
));
}
#[test]
fn test_get_underlying_type_fallback() {
let mut thread = create_test_thread();
let result = nullable_get_underlying_type_pre(
&HookContext::new(
Token::new(0x0A000001),
"System",
"Nullable",
"GetUnderlyingType",
PointerSize::Bit64,
),
&mut thread,
);
assert!(matches!(result, PreHookResult::Bypass(Some(EmValue::Null))));
}
}