use crate::{
emulation::{
runtime::hook::{Hook, HookContext, HookManager, PreHookResult},
thread::EmulationThread,
EmValue,
},
utils::{apply_crypto_transform, decompress_deflate, decompress_gzip},
Result,
};
pub fn register(manager: &HookManager) -> Result<()> {
manager.register(
Hook::new("System.IO.MemoryStream..ctor")
.match_name("System.IO", "MemoryStream", ".ctor")
.pre(memory_stream_ctor_pre),
)?;
manager.register(
Hook::new("System.IO.MemoryStream.Read")
.match_name("System.IO", "MemoryStream", "Read")
.pre(stream_read_pre),
)?;
manager.register(
Hook::new("System.IO.MemoryStream.ReadByte")
.match_name("System.IO", "MemoryStream", "ReadByte")
.pre(stream_read_byte_pre),
)?;
manager.register(
Hook::new("System.IO.MemoryStream.Write")
.match_name("System.IO", "MemoryStream", "Write")
.pre(stream_write_pre),
)?;
manager.register(
Hook::new("System.IO.MemoryStream.ToArray")
.match_name("System.IO", "MemoryStream", "ToArray")
.pre(memory_stream_to_array_pre),
)?;
manager.register(
Hook::new("System.IO.MemoryStream.get_Length")
.match_name("System.IO", "MemoryStream", "get_Length")
.pre(stream_get_length_pre),
)?;
manager.register(
Hook::new("System.IO.MemoryStream.get_Position")
.match_name("System.IO", "MemoryStream", "get_Position")
.pre(stream_get_position_pre),
)?;
manager.register(
Hook::new("System.IO.MemoryStream.set_Position")
.match_name("System.IO", "MemoryStream", "set_Position")
.pre(stream_set_position_pre),
)?;
manager.register(
Hook::new("System.IO.MemoryStream.WriteByte")
.match_name("System.IO", "MemoryStream", "WriteByte")
.pre(stream_write_byte_pre),
)?;
manager.register(
Hook::new("System.IO.MemoryStream.Seek")
.match_name("System.IO", "MemoryStream", "Seek")
.pre(stream_seek_pre),
)?;
manager.register(
Hook::new("System.IO.MemoryStream.GetBuffer")
.match_name("System.IO", "MemoryStream", "GetBuffer")
.pre(memory_stream_get_buffer_pre),
)?;
manager.register(
Hook::new("System.IO.MemoryStream.SetLength")
.match_name("System.IO", "MemoryStream", "SetLength")
.pre(stream_set_length_pre),
)?;
manager.register(
Hook::new("System.IO.MemoryStream.get_Capacity")
.match_name("System.IO", "MemoryStream", "get_Capacity")
.pre(stream_get_length_pre),
)?;
manager.register(
Hook::new("System.IO.MemoryStream.CopyTo")
.match_name("System.IO", "MemoryStream", "CopyTo")
.pre(stream_copy_to_pre),
)?;
manager.register(
Hook::new("System.IO.MemoryStream.Flush")
.match_name("System.IO", "MemoryStream", "Flush")
.pre(stream_close_pre),
)?;
manager.register(
Hook::new("System.IO.MemoryStream.Close")
.match_name("System.IO", "MemoryStream", "Close")
.pre(stream_close_pre),
)?;
manager.register(
Hook::new("System.IO.MemoryStream.Dispose")
.match_name("System.IO", "MemoryStream", "Dispose")
.pre(stream_dispose_pre),
)?;
manager.register(
Hook::new("System.IO.MemoryStream.get_CanRead")
.match_name("System.IO", "MemoryStream", "get_CanRead")
.pre(stream_can_true_pre),
)?;
manager.register(
Hook::new("System.IO.MemoryStream.get_CanSeek")
.match_name("System.IO", "MemoryStream", "get_CanSeek")
.pre(stream_can_true_pre),
)?;
manager.register(
Hook::new("System.IO.MemoryStream.get_CanWrite")
.match_name("System.IO", "MemoryStream", "get_CanWrite")
.pre(stream_can_true_pre),
)?;
manager.register(
Hook::new("System.IO.Stream.Read")
.match_name("System.IO", "Stream", "Read")
.pre(stream_read_pre),
)?;
manager.register(
Hook::new("System.IO.Stream.ReadByte")
.match_name("System.IO", "Stream", "ReadByte")
.pre(stream_read_byte_pre),
)?;
manager.register(
Hook::new("System.IO.Stream.get_Length")
.match_name("System.IO", "Stream", "get_Length")
.pre(stream_get_length_pre),
)?;
manager.register(
Hook::new("System.IO.Stream.get_Position")
.match_name("System.IO", "Stream", "get_Position")
.pre(stream_get_position_pre),
)?;
manager.register(
Hook::new("System.IO.Stream.set_Position")
.match_name("System.IO", "Stream", "set_Position")
.pre(stream_set_position_pre),
)?;
manager.register(
Hook::new("System.IO.Stream.Write")
.match_name("System.IO", "Stream", "Write")
.pre(stream_write_pre),
)?;
manager.register(
Hook::new("System.IO.Stream.WriteByte")
.match_name("System.IO", "Stream", "WriteByte")
.pre(stream_write_byte_pre),
)?;
manager.register(
Hook::new("System.IO.Stream.Seek")
.match_name("System.IO", "Stream", "Seek")
.pre(stream_seek_pre),
)?;
manager.register(
Hook::new("System.IO.Stream.SetLength")
.match_name("System.IO", "Stream", "SetLength")
.pre(stream_set_length_pre),
)?;
manager.register(
Hook::new("System.IO.Stream.CopyTo")
.match_name("System.IO", "Stream", "CopyTo")
.pre(stream_copy_to_pre),
)?;
manager.register(
Hook::new("System.IO.Stream.Flush")
.match_name("System.IO", "Stream", "Flush")
.pre(stream_close_pre),
)?;
manager.register(
Hook::new("System.IO.Stream.Close")
.match_name("System.IO", "Stream", "Close")
.pre(stream_close_pre),
)?;
manager.register(
Hook::new("System.IO.Stream.Dispose")
.match_name("System.IO", "Stream", "Dispose")
.pre(stream_dispose_pre),
)?;
manager.register(
Hook::new("System.IO.Stream.get_CanRead")
.match_name("System.IO", "Stream", "get_CanRead")
.pre(stream_can_true_pre),
)?;
manager.register(
Hook::new("System.IO.Stream.get_CanSeek")
.match_name("System.IO", "Stream", "get_CanSeek")
.pre(stream_can_true_pre),
)?;
manager.register(
Hook::new("System.IO.Stream.get_CanWrite")
.match_name("System.IO", "Stream", "get_CanWrite")
.pre(stream_can_true_pre),
)?;
Ok(())
}
fn memory_stream_ctor_pre(ctx: &HookContext<'_>, thread: &mut EmulationThread) -> PreHookResult {
let data = if let Some(EmValue::ObjectRef(array_ref)) = ctx.args.first() {
let primary = try_hook!(thread.heap().get_byte_array(*array_ref));
if let Some(bytes) = primary {
bytes
} else {
try_hook!(thread
.heap()
.get_array_as_bytes(*array_ref, ctx.pointer_size))
.unwrap_or_default()
}
} else {
Vec::new()
};
match ctx.this {
Some(EmValue::ObjectRef(stream_ref)) => {
try_hook!(thread.heap_mut().replace_with_stream(*stream_ref, data));
PreHookResult::Bypass(None) }
_ => {
let type_token = thread.resolve_type_token("System.IO", "MemoryStream");
match thread.heap_mut().alloc_stream(data, type_token) {
Ok(stream_ref) => PreHookResult::Bypass(Some(EmValue::ObjectRef(stream_ref))),
Err(e) => PreHookResult::Error(format!("heap allocation failed: {e}")),
}
}
}
}
fn stream_read_pre(ctx: &HookContext<'_>, thread: &mut EmulationThread) -> PreHookResult {
let stream_ref = match ctx.this {
Some(EmValue::ObjectRef(r)) => *r,
Some(EmValue::Null) => return PreHookResult::throw_null_reference(),
_ => return PreHookResult::throw_object_disposed(),
};
#[allow(clippy::cast_sign_loss)]
let (buffer_ref, offset, count) = match (ctx.args.first(), ctx.args.get(1), ctx.args.get(2)) {
(Some(EmValue::ObjectRef(b)), Some(EmValue::I32(o)), Some(EmValue::I32(c))) => {
(*b, *o as usize, *c as usize)
}
_ => return PreHookResult::Bypass(Some(EmValue::I32(0))),
};
let Some(bytes) = try_hook!(thread.heap().stream_read(stream_ref, count)) else {
return PreHookResult::Bypass(Some(EmValue::I32(0)));
};
let to_read = bytes.len();
for (i, &byte) in bytes.iter().enumerate() {
try_hook!(thread.heap_mut().set_array_element(
buffer_ref,
offset + i,
EmValue::I32(i32::from(byte)),
));
}
#[allow(clippy::cast_possible_wrap, clippy::cast_possible_truncation)]
PreHookResult::Bypass(Some(EmValue::I32(to_read as i32)))
}
fn stream_read_byte_pre(ctx: &HookContext<'_>, thread: &mut EmulationThread) -> PreHookResult {
let stream_ref = match ctx.this {
Some(EmValue::ObjectRef(r)) => *r,
_ => return PreHookResult::Bypass(Some(EmValue::I32(-1))),
};
let Some(byte) = try_hook!(thread.heap().stream_read_byte(stream_ref)) else {
return PreHookResult::Bypass(Some(EmValue::I32(-1))); };
PreHookResult::Bypass(Some(EmValue::I32(i32::from(byte))))
}
fn stream_write_pre(ctx: &HookContext<'_>, thread: &mut EmulationThread) -> PreHookResult {
let stream_ref = match ctx.this {
Some(EmValue::ObjectRef(r)) => *r,
_ => return PreHookResult::Bypass(None),
};
#[allow(clippy::cast_sign_loss)]
let (buffer_ref, offset, count) = match (ctx.args.first(), ctx.args.get(1), ctx.args.get(2)) {
(Some(EmValue::ObjectRef(b)), Some(EmValue::I32(o)), Some(EmValue::I32(c))) => {
(*b, *o as usize, *c as usize)
}
_ => return PreHookResult::Bypass(None),
};
let Some(buffer_data) = try_hook!(thread.heap().get_byte_array(buffer_ref)) else {
return PreHookResult::Bypass(None);
};
let end = (offset + count).min(buffer_data.len());
let bytes_to_write = if offset < buffer_data.len() {
&buffer_data[offset..end]
} else {
&[]
};
try_hook!(thread
.heap_mut()
.write_to_stream(stream_ref, bytes_to_write));
PreHookResult::Bypass(None)
}
fn memory_stream_to_array_pre(
ctx: &HookContext<'_>,
thread: &mut EmulationThread,
) -> PreHookResult {
let stream_ref = match ctx.this {
Some(EmValue::ObjectRef(r)) => *r,
_ => {
match thread.heap_mut().alloc_byte_array(&[]) {
Ok(array_ref) => return PreHookResult::Bypass(Some(EmValue::ObjectRef(array_ref))),
Err(e) => return PreHookResult::Error(format!("heap allocation failed: {e}")),
}
}
};
let data = match try_hook!(thread.heap().get_stream_data(stream_ref)) {
Some((data, _)) => data,
None => Vec::new(),
};
match thread.heap_mut().alloc_byte_array(&data) {
Ok(array_ref) => PreHookResult::Bypass(Some(EmValue::ObjectRef(array_ref))),
Err(e) => PreHookResult::Error(format!("heap allocation failed: {e}")),
}
}
fn stream_get_length_pre(ctx: &HookContext<'_>, thread: &mut EmulationThread) -> PreHookResult {
let stream_ref = match ctx.this {
Some(EmValue::ObjectRef(r)) => *r,
Some(EmValue::Null) => return PreHookResult::throw_null_reference(),
_ => return PreHookResult::throw_object_disposed(),
};
let length = try_hook!(thread.heap().stream_len(stream_ref)).unwrap_or(0);
#[allow(clippy::cast_possible_wrap)]
PreHookResult::Bypass(Some(EmValue::I64(length as i64)))
}
fn stream_get_position_pre(ctx: &HookContext<'_>, thread: &mut EmulationThread) -> PreHookResult {
let stream_ref = match ctx.this {
Some(EmValue::ObjectRef(r)) => *r,
Some(EmValue::Null) => return PreHookResult::throw_null_reference(),
_ => return PreHookResult::throw_object_disposed(),
};
let position = try_hook!(thread.heap().stream_position(stream_ref)).unwrap_or(0);
#[allow(clippy::cast_possible_wrap)]
PreHookResult::Bypass(Some(EmValue::I64(position as i64)))
}
fn stream_set_position_pre(ctx: &HookContext<'_>, thread: &mut EmulationThread) -> PreHookResult {
let stream_ref = match ctx.this {
Some(EmValue::ObjectRef(r)) => *r,
_ => return PreHookResult::Bypass(None),
};
#[allow(clippy::cast_possible_truncation, clippy::cast_sign_loss)]
let new_pos = match ctx.args.first() {
Some(EmValue::I64(v)) => *v as usize,
Some(EmValue::I32(v)) => *v as usize,
_ => return PreHookResult::Bypass(None),
};
try_hook!(thread.heap_mut().set_stream_position(stream_ref, new_pos));
PreHookResult::Bypass(None)
}
fn stream_seek_pre(ctx: &HookContext<'_>, thread: &mut EmulationThread) -> PreHookResult {
let stream_ref = match ctx.this {
Some(EmValue::ObjectRef(r)) => *r,
Some(EmValue::Null) => return PreHookResult::throw_null_reference(),
_ => return PreHookResult::throw_object_disposed(),
};
let offset = match ctx.args.first() {
Some(EmValue::I64(v)) => *v,
Some(EmValue::I32(v)) => i64::from(*v),
_ => 0,
};
let origin = match ctx.args.get(1) {
Some(EmValue::I32(v)) => *v,
_ => 0, };
let Some(length) = try_hook!(thread.heap().stream_len(stream_ref)) else {
return PreHookResult::Bypass(Some(EmValue::I64(0)));
};
let current_pos = try_hook!(thread.heap().stream_position(stream_ref)).unwrap_or(0);
#[allow(clippy::cast_possible_truncation, clippy::cast_sign_loss)]
let new_pos = match origin {
1 => {
#[allow(clippy::cast_possible_wrap)]
let pos = current_pos as i64 + offset;
pos.max(0) as usize
}
2 => {
#[allow(clippy::cast_possible_wrap)]
let pos = length as i64 + offset;
pos.max(0) as usize
}
_ => offset.max(0) as usize,
};
let clamped_pos = new_pos.min(length);
try_hook!(thread
.heap_mut()
.set_stream_position(stream_ref, clamped_pos));
#[allow(clippy::cast_possible_wrap)]
PreHookResult::Bypass(Some(EmValue::I64(clamped_pos as i64)))
}
fn stream_write_byte_pre(ctx: &HookContext<'_>, thread: &mut EmulationThread) -> PreHookResult {
let stream_ref = match ctx.this {
Some(EmValue::ObjectRef(r)) => *r,
_ => return PreHookResult::Bypass(None),
};
#[allow(clippy::cast_sign_loss, clippy::cast_possible_truncation)]
let byte = match ctx.args.first() {
Some(EmValue::I32(v)) => *v as u8,
_ => return PreHookResult::Bypass(None),
};
try_hook!(thread.heap_mut().write_to_stream(stream_ref, &[byte]));
PreHookResult::Bypass(None)
}
fn memory_stream_get_buffer_pre(
ctx: &HookContext<'_>,
thread: &mut EmulationThread,
) -> PreHookResult {
memory_stream_to_array_pre(ctx, thread)
}
fn stream_set_length_pre(ctx: &HookContext<'_>, thread: &mut EmulationThread) -> PreHookResult {
let stream_ref = match ctx.this {
Some(EmValue::ObjectRef(r)) => *r,
_ => return PreHookResult::Bypass(None),
};
#[allow(clippy::cast_possible_truncation, clippy::cast_sign_loss)]
let new_length = match ctx.args.first() {
Some(EmValue::I64(v)) => *v as usize,
Some(EmValue::I32(v)) => *v as usize,
_ => return PreHookResult::Bypass(None),
};
try_hook!(thread.heap_mut().truncate_stream(stream_ref, new_length));
PreHookResult::Bypass(None)
}
fn stream_copy_to_pre(ctx: &HookContext<'_>, thread: &mut EmulationThread) -> PreHookResult {
let src_ref = match ctx.this {
Some(EmValue::ObjectRef(r)) => *r,
_ => return PreHookResult::Bypass(None),
};
let dst_ref = match ctx.args.first() {
Some(EmValue::ObjectRef(r)) => *r,
_ => return PreHookResult::Bypass(None),
};
if let Some((data, position)) = try_hook!(thread.heap().get_stream_data(src_ref)) {
if position < data.len() {
let remaining = &data[position..];
try_hook!(thread.heap_mut().write_to_stream(dst_ref, remaining));
}
let len = data.len();
try_hook!(thread.heap_mut().set_stream_position(src_ref, len));
return PreHookResult::Bypass(None);
}
if let Some((underlying, compression_type, _mode)) =
try_hook!(thread.heap().get_compressed_stream_info(src_ref))
{
if !try_hook!(thread.heap().has_compressed_stream_data(src_ref)) {
let Some((compressed_data, underlying_pos)) =
try_hook!(thread.heap().get_stream_data(underlying))
else {
return PreHookResult::Bypass(None);
};
let effective = if underlying_pos < compressed_data.len() {
&compressed_data[underlying_pos..]
} else {
&[]
};
let decompressed = match compression_type {
0 => decompress_deflate(effective).ok(),
1 => decompress_gzip(effective).ok(),
_ => None,
};
try_hook!(thread
.heap()
.set_compressed_stream_data(src_ref, decompressed.unwrap_or_default()));
}
let Some(bytes) = try_hook!(thread.heap().read_compressed_stream(src_ref, usize::MAX))
else {
return PreHookResult::Bypass(None);
};
if !bytes.is_empty() {
try_hook!(thread.heap_mut().write_to_stream(dst_ref, &bytes));
}
return PreHookResult::Bypass(None);
}
if let Some((underlying, transform, _mode)) =
try_hook!(thread.heap().get_crypto_stream_info(src_ref))
{
if try_hook!(thread.heap().get_crypto_stream_transformed(src_ref)).is_none() {
let Some((stream_data, underlying_pos)) =
try_hook!(thread.heap().get_stream_data(underlying))
else {
return PreHookResult::Bypass(None);
};
let effective = if underlying_pos < stream_data.len() {
&stream_data[underlying_pos..]
} else {
&[]
};
let transformed = if let Some((algorithm, key, iv, is_encryptor, cmode, padding)) =
try_hook!(thread.heap().get_crypto_transform_info(transform))
{
apply_crypto_transform(
&algorithm,
&key,
&iv,
is_encryptor,
effective,
cmode,
padding,
)
.unwrap_or_else(|| effective.to_vec())
} else {
effective.to_vec()
};
try_hook!(thread
.heap()
.set_crypto_stream_transformed(src_ref, transformed));
}
let Some(bytes) = try_hook!(thread.heap().read_crypto_stream(src_ref, usize::MAX)) else {
return PreHookResult::Bypass(None);
};
if !bytes.is_empty() {
try_hook!(thread.heap_mut().write_to_stream(dst_ref, &bytes));
}
return PreHookResult::Bypass(None);
}
PreHookResult::Bypass(None)
}
pub(crate) fn stream_can_true_pre(
_ctx: &HookContext<'_>,
_thread: &mut EmulationThread,
) -> PreHookResult {
PreHookResult::Bypass(Some(EmValue::I32(1)))
}
pub(crate) fn stream_close_pre(
_ctx: &HookContext<'_>,
_thread: &mut EmulationThread,
) -> PreHookResult {
PreHookResult::Bypass(None)
}
pub(crate) fn stream_dispose_pre(
_ctx: &HookContext<'_>,
_thread: &mut EmulationThread,
) -> PreHookResult {
PreHookResult::Bypass(None)
}
#[cfg(test)]
mod tests {
use crate::{
emulation::{
runtime::{
bcl::io::stream::{
memory_stream_ctor_pre, memory_stream_to_array_pre, stream_close_pre,
stream_get_length_pre, stream_get_position_pre, stream_read_byte_pre,
stream_seek_pre, stream_set_position_pre,
},
hook::{HookContext, HookManager, PreHookResult},
},
EmValue,
},
metadata::{token::Token, typesystem::PointerSize},
test::emulation::create_test_thread,
};
#[test]
fn test_register_hooks() {
let manager = HookManager::new();
super::register(&manager).unwrap();
assert_eq!(manager.len(), 36);
}
#[test]
fn test_stream_close_hook() {
let ctx = HookContext::new(
Token::new(0x0A000001),
"System.IO",
"Stream",
"Close",
PointerSize::Bit64,
);
let mut thread = create_test_thread();
let result = stream_close_pre(&ctx, &mut thread);
match result {
PreHookResult::Bypass(None) => {}
_ => panic!("Expected Bypass(None)"),
}
}
#[test]
fn test_stream_read_byte_eof_without_stream() {
let ctx = HookContext::new(
Token::new(0x0A000001),
"System.IO",
"Stream",
"ReadByte",
PointerSize::Bit64,
);
let mut thread = create_test_thread();
let result = stream_read_byte_pre(&ctx, &mut thread);
match result {
PreHookResult::Bypass(Some(EmValue::I32(-1))) => {}
_ => panic!("Expected Bypass with I32(-1) for EOF"),
}
}
#[test]
fn test_stream_read_byte_with_data() {
let mut thread = create_test_thread();
let data = vec![1, 2, 3, 4, 5];
let stream_ref = thread.heap_mut().alloc_stream(data, None).unwrap();
let this = EmValue::ObjectRef(stream_ref);
let ctx = HookContext::new(
Token::new(0x0A000001),
"System.IO",
"Stream",
"ReadByte",
PointerSize::Bit64,
)
.with_this(Some(&this));
let result = stream_read_byte_pre(&ctx, &mut thread);
assert!(matches!(
result,
PreHookResult::Bypass(Some(EmValue::I32(1)))
));
let result = stream_read_byte_pre(&ctx, &mut thread);
assert!(matches!(
result,
PreHookResult::Bypass(Some(EmValue::I32(2)))
));
for expected in [3, 4, 5] {
let result = stream_read_byte_pre(&ctx, &mut thread);
assert!(
matches!(result, PreHookResult::Bypass(Some(EmValue::I32(v))) if v == expected)
);
}
let result = stream_read_byte_pre(&ctx, &mut thread);
assert!(matches!(
result,
PreHookResult::Bypass(Some(EmValue::I32(-1)))
));
}
#[test]
fn test_stream_get_length_hook() {
let mut thread = create_test_thread();
let data = vec![1, 2, 3, 4, 5];
let stream_ref = thread.heap_mut().alloc_stream(data, None).unwrap();
let this = EmValue::ObjectRef(stream_ref);
let ctx = HookContext::new(
Token::new(0x0A000001),
"System.IO",
"Stream",
"get_Length",
PointerSize::Bit64,
)
.with_this(Some(&this));
let result = stream_get_length_pre(&ctx, &mut thread);
assert!(matches!(
result,
PreHookResult::Bypass(Some(EmValue::I64(5)))
));
}
#[test]
fn test_stream_get_position_hook() {
let mut thread = create_test_thread();
let data = vec![1, 2, 3, 4, 5];
let stream_ref = thread.heap_mut().alloc_stream(data, None).unwrap();
let this = EmValue::ObjectRef(stream_ref);
let get_pos_ctx = HookContext::new(
Token::new(0x0A000001),
"System.IO",
"Stream",
"get_Position",
PointerSize::Bit64,
)
.with_this(Some(&this));
let read_byte_ctx = HookContext::new(
Token::new(0x0A000002),
"System.IO",
"Stream",
"ReadByte",
PointerSize::Bit64,
)
.with_this(Some(&this));
let result = stream_get_position_pre(&get_pos_ctx, &mut thread);
assert!(matches!(
result,
PreHookResult::Bypass(Some(EmValue::I64(0)))
));
stream_read_byte_pre(&read_byte_ctx, &mut thread);
let result = stream_get_position_pre(&get_pos_ctx, &mut thread);
assert!(matches!(
result,
PreHookResult::Bypass(Some(EmValue::I64(1)))
));
}
#[test]
fn test_stream_set_position_hook() {
let mut thread = create_test_thread();
let data = vec![1, 2, 3, 4, 5];
let stream_ref = thread.heap_mut().alloc_stream(data, None).unwrap();
let this = EmValue::ObjectRef(stream_ref);
let args = [EmValue::I64(3)];
let set_pos_ctx = HookContext::new(
Token::new(0x0A000001),
"System.IO",
"Stream",
"set_Position",
PointerSize::Bit64,
)
.with_this(Some(&this))
.with_args(&args);
stream_set_position_pre(&set_pos_ctx, &mut thread);
let get_pos_ctx = HookContext::new(
Token::new(0x0A000002),
"System.IO",
"Stream",
"get_Position",
PointerSize::Bit64,
)
.with_this(Some(&this));
let result = stream_get_position_pre(&get_pos_ctx, &mut thread);
assert!(matches!(
result,
PreHookResult::Bypass(Some(EmValue::I64(3)))
));
let read_byte_ctx = HookContext::new(
Token::new(0x0A000003),
"System.IO",
"Stream",
"ReadByte",
PointerSize::Bit64,
)
.with_this(Some(&this));
let result = stream_read_byte_pre(&read_byte_ctx, &mut thread);
assert!(matches!(
result,
PreHookResult::Bypass(Some(EmValue::I32(4)))
));
}
#[test]
fn test_memory_stream_to_array_hook() {
let mut thread = create_test_thread();
let data = vec![10, 20, 30];
let stream_ref = thread.heap_mut().alloc_stream(data.clone(), None).unwrap();
let this = EmValue::ObjectRef(stream_ref);
let ctx = HookContext::new(
Token::new(0x0A000001),
"System.IO",
"MemoryStream",
"ToArray",
PointerSize::Bit64,
)
.with_this(Some(&this));
let result = memory_stream_to_array_pre(&ctx, &mut thread);
let array_ref = match result {
PreHookResult::Bypass(Some(EmValue::ObjectRef(r))) => r,
_ => panic!("Expected Bypass with ObjectRef"),
};
let retrieved = thread.heap().get_byte_array(array_ref).unwrap().unwrap();
assert_eq!(retrieved.as_slice(), &[10, 20, 30]);
}
#[test]
fn test_memory_stream_ctor_hook() {
let mut thread = create_test_thread();
let data = vec![1, 2, 3, 4, 5];
let array_ref = thread.heap_mut().alloc_byte_array(&data).unwrap();
let args = [EmValue::ObjectRef(array_ref)];
let ctx = HookContext::new(
Token::new(0x0A000001),
"System.IO",
"MemoryStream",
".ctor",
PointerSize::Bit64,
)
.with_args(&args);
let result = memory_stream_ctor_pre(&ctx, &mut thread);
let stream_ref = match result {
PreHookResult::Bypass(Some(EmValue::ObjectRef(r))) => r,
_ => panic!("Expected Bypass with ObjectRef"),
};
let this = EmValue::ObjectRef(stream_ref);
let length_ctx = HookContext::new(
Token::new(0x0A000002),
"System.IO",
"MemoryStream",
"get_Length",
PointerSize::Bit64,
)
.with_this(Some(&this));
let result = stream_get_length_pre(&length_ctx, &mut thread);
assert!(matches!(
result,
PreHookResult::Bypass(Some(EmValue::I64(5)))
));
let read_ctx = HookContext::new(
Token::new(0x0A000003),
"System.IO",
"MemoryStream",
"ReadByte",
PointerSize::Bit64,
)
.with_this(Some(&this));
let result = stream_read_byte_pre(&read_ctx, &mut thread);
assert!(matches!(
result,
PreHookResult::Bypass(Some(EmValue::I32(1)))
));
}
#[test]
fn test_stream_seek_hook() {
let mut thread = create_test_thread();
let data = vec![10, 20, 30, 40, 50];
let stream_ref = thread.heap_mut().alloc_stream(data, None).unwrap();
let this = EmValue::ObjectRef(stream_ref);
let args = [EmValue::I64(2), EmValue::I32(0)];
let seek_ctx = HookContext::new(
Token::new(0x0A000001),
"System.IO",
"MemoryStream",
"Seek",
PointerSize::Bit64,
)
.with_this(Some(&this))
.with_args(&args);
let result = stream_seek_pre(&seek_ctx, &mut thread);
assert!(matches!(
result,
PreHookResult::Bypass(Some(EmValue::I64(2)))
));
let read_ctx = HookContext::new(
Token::new(0x0A000002),
"System.IO",
"MemoryStream",
"ReadByte",
PointerSize::Bit64,
)
.with_this(Some(&this));
let result = stream_read_byte_pre(&read_ctx, &mut thread);
assert!(matches!(
result,
PreHookResult::Bypass(Some(EmValue::I32(30)))
));
let args = [EmValue::I64(-1), EmValue::I32(2)];
let seek_ctx = HookContext::new(
Token::new(0x0A000003),
"System.IO",
"MemoryStream",
"Seek",
PointerSize::Bit64,
)
.with_this(Some(&this))
.with_args(&args);
let result = stream_seek_pre(&seek_ctx, &mut thread);
assert!(matches!(
result,
PreHookResult::Bypass(Some(EmValue::I64(4)))
));
let result = stream_read_byte_pre(&read_ctx, &mut thread);
assert!(matches!(
result,
PreHookResult::Bypass(Some(EmValue::I32(50)))
));
}
}