use std::fmt;
use crate::{
emulation::{engine::stats::LimitExceeded, EmValue},
metadata::token::Token,
};
#[derive(Clone, Debug)]
pub enum StepResult {
Continue,
Branch {
target: u64,
},
Return {
value: Option<EmValue>,
},
Call {
method: Token,
args: Vec<EmValue>,
is_virtual: bool,
},
NewObj {
constructor: Token,
args: Vec<EmValue>,
},
NewArray {
element_type: Token,
length: usize,
},
LoadString {
token: Token,
},
LoadStaticField {
field: Token,
},
StoreStaticField {
field: Token,
value: EmValue,
},
Throw {
exception: EmValue,
},
Leave {
target: u64,
},
EndFinally,
EndFilter {
value: EmValue,
},
Rethrow,
Breakpoint,
TailCall {
method: Token,
args: Vec<EmValue>,
},
CopyObject {
type_token: Token,
},
CastClass {
type_token: Token,
},
IsInst {
type_token: Token,
},
Unbox {
type_token: Token,
},
Box {
type_token: Token,
},
UnboxAny {
type_token: Token,
},
RefAnyVal {
type_token: Token,
},
MkRefAny {
type_token: Token,
},
LoadToken {
token: Token,
},
LoadFunctionPointer {
method: Token,
},
LoadVirtualFunctionPointer {
method: Token,
},
CallIndirect {
signature: Token,
function_pointer: EmValue,
},
LocalAlloc {
size: EmValue,
},
InitObj {
type_token: Token,
},
CopyBlock {
dest: EmValue,
src: EmValue,
size: EmValue,
},
InitBlock {
addr: EmValue,
value: EmValue,
size: EmValue,
},
SizeOf {
type_token: Token,
},
RefAnyType,
ArgList,
}
impl StepResult {
#[must_use]
pub fn is_continue(&self) -> bool {
matches!(self, StepResult::Continue)
}
#[must_use]
pub fn is_return(&self) -> bool {
matches!(self, StepResult::Return { .. })
}
#[must_use]
pub fn is_control_transfer(&self) -> bool {
!matches!(self, StepResult::Continue | StepResult::Breakpoint)
}
#[must_use]
pub fn branch_target(&self) -> Option<u64> {
match self {
StepResult::Branch { target } | StepResult::Leave { target } => Some(*target),
_ => None,
}
}
}
impl fmt::Display for StepResult {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
StepResult::Continue => write!(f, "continue"),
StepResult::Branch { target } => write!(f, "branch to 0x{target:08X}"),
StepResult::Return { value } => {
if let Some(v) = value {
write!(f, "return {v:?}")
} else {
write!(f, "return void")
}
}
StepResult::Call {
method,
args,
is_virtual,
} => {
let call_type = if *is_virtual { "callvirt" } else { "call" };
write!(
f,
"{call_type} 0x{:08X} with {} args",
method.value(),
args.len()
)
}
StepResult::NewObj { constructor, args } => {
write!(
f,
"newobj 0x{:08X} with {} args",
constructor.value(),
args.len()
)
}
StepResult::NewArray {
element_type,
length,
} => {
write!(f, "newarr 0x{:08X} length {length}", element_type.value())
}
StepResult::LoadString { token } => {
write!(f, "ldstr 0x{:08X}", token.value())
}
StepResult::LoadStaticField { field } => {
write!(f, "ldsfld 0x{:08X}", field.value())
}
StepResult::StoreStaticField { field, .. } => {
write!(f, "stsfld 0x{:08X}", field.value())
}
StepResult::Throw { exception } => write!(f, "throw {exception:?}"),
StepResult::Leave { target } => write!(f, "leave to 0x{target:08X}"),
StepResult::EndFinally => write!(f, "endfinally"),
StepResult::EndFilter { value } => write!(f, "endfilter {value:?}"),
StepResult::Rethrow => write!(f, "rethrow"),
StepResult::Breakpoint => write!(f, "breakpoint"),
StepResult::TailCall { method, args } => {
write!(
f,
"tail.call 0x{:08X} with {} args",
method.value(),
args.len()
)
}
StepResult::CopyObject { type_token } => {
write!(f, "cpobj 0x{:08X}", type_token.value())
}
StepResult::CastClass { type_token } => {
write!(f, "castclass 0x{:08X}", type_token.value())
}
StepResult::IsInst { type_token } => {
write!(f, "isinst 0x{:08X}", type_token.value())
}
StepResult::Unbox { type_token } => {
write!(f, "unbox 0x{:08X}", type_token.value())
}
StepResult::Box { type_token } => {
write!(f, "box 0x{:08X}", type_token.value())
}
StepResult::UnboxAny { type_token } => {
write!(f, "unbox.any 0x{:08X}", type_token.value())
}
StepResult::RefAnyVal { type_token } => {
write!(f, "refanyval 0x{:08X}", type_token.value())
}
StepResult::MkRefAny { type_token } => {
write!(f, "mkrefany 0x{:08X}", type_token.value())
}
StepResult::LoadToken { token } => {
write!(f, "ldtoken 0x{:08X}", token.value())
}
StepResult::LoadFunctionPointer { method } => {
write!(f, "ldftn 0x{:08X}", method.value())
}
StepResult::LoadVirtualFunctionPointer { method } => {
write!(f, "ldvirtftn 0x{:08X}", method.value())
}
StepResult::CallIndirect { signature, .. } => {
write!(f, "calli 0x{:08X}", signature.value())
}
StepResult::LocalAlloc { size } => {
write!(f, "localloc {size:?}")
}
StepResult::InitObj { type_token } => {
write!(f, "initobj 0x{:08X}", type_token.value())
}
StepResult::CopyBlock { .. } => write!(f, "cpblk"),
StepResult::InitBlock { .. } => write!(f, "initblk"),
StepResult::SizeOf { type_token } => {
write!(f, "sizeof 0x{:08X}", type_token.value())
}
StepResult::RefAnyType => write!(f, "refanytype"),
StepResult::ArgList => write!(f, "arglist"),
}
}
}
#[derive(Clone, Debug)]
pub enum EmulationOutcome {
Completed {
return_value: Option<EmValue>,
instructions: u64,
},
UnhandledException {
exception: EmValue,
instructions: u64,
},
LimitReached {
limit: LimitExceeded,
partial_state: Option<Box<EmValue>>,
},
RequiresSymbolic {
reason: String,
value: EmValue,
instructions: u64,
},
Breakpoint {
offset: u32,
instructions: u64,
},
Stopped {
reason: String,
instructions: u64,
},
}
impl EmulationOutcome {
#[must_use]
pub fn is_completed(&self) -> bool {
matches!(self, EmulationOutcome::Completed { .. })
}
#[must_use]
pub fn return_value(&self) -> Option<&EmValue> {
match self {
EmulationOutcome::Completed { return_value, .. } => return_value.as_ref(),
_ => None,
}
}
#[must_use]
pub fn instructions_executed(&self) -> u64 {
match self {
EmulationOutcome::LimitReached { .. } => 0, EmulationOutcome::Completed { instructions, .. }
| EmulationOutcome::UnhandledException { instructions, .. }
| EmulationOutcome::RequiresSymbolic { instructions, .. }
| EmulationOutcome::Breakpoint { instructions, .. }
| EmulationOutcome::Stopped { instructions, .. } => *instructions,
}
}
}
impl fmt::Display for EmulationOutcome {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
EmulationOutcome::Completed {
return_value,
instructions,
} => {
if let Some(value) = return_value {
write!(
f,
"completed with {value:?} after {instructions} instructions"
)
} else {
write!(f, "completed (void) after {instructions} instructions")
}
}
EmulationOutcome::UnhandledException {
exception,
instructions,
} => {
write!(
f,
"unhandled exception {exception:?} after {instructions} instructions"
)
}
EmulationOutcome::LimitReached { limit, .. } => {
write!(f, "limit reached: {limit}")
}
EmulationOutcome::RequiresSymbolic { reason, .. } => {
write!(f, "requires symbolic execution: {reason}")
}
EmulationOutcome::Breakpoint {
offset,
instructions,
} => {
write!(
f,
"breakpoint at 0x{offset:08X} after {instructions} instructions"
)
}
EmulationOutcome::Stopped {
reason,
instructions,
} => {
write!(f, "stopped: {reason} after {instructions} instructions")
}
}
}
}
#[cfg(test)]
mod tests {
use std::time::Duration;
use super::*;
#[test]
fn test_step_result_continue() {
let result = StepResult::Continue;
assert!(result.is_continue());
assert!(!result.is_return());
assert!(!result.is_control_transfer());
}
#[test]
fn test_step_result_return() {
let result = StepResult::Return {
value: Some(EmValue::I32(42)),
};
assert!(!result.is_continue());
assert!(result.is_return());
assert!(result.is_control_transfer());
}
#[test]
fn test_step_result_branch() {
let result = StepResult::Branch { target: 0x1000 };
assert!(result.is_control_transfer());
assert_eq!(result.branch_target(), Some(0x1000));
}
#[test]
fn test_step_result_call() {
let result = StepResult::Call {
method: Token::new(0x06000001),
args: vec![EmValue::I32(1), EmValue::I32(2)],
is_virtual: false,
};
assert!(result.is_control_transfer());
let display = format!("{result}");
assert!(display.contains("call"));
assert!(display.contains("2 args"));
}
#[test]
fn test_step_result_display() {
let results = vec![
StepResult::Continue,
StepResult::Branch { target: 0x100 },
StepResult::Return { value: None },
StepResult::Return {
value: Some(EmValue::I32(42)),
},
StepResult::Throw {
exception: EmValue::Null,
},
StepResult::Leave { target: 0x200 },
StepResult::EndFinally,
StepResult::EndFilter {
value: EmValue::I32(1),
},
StepResult::Rethrow,
StepResult::Breakpoint,
];
for result in results {
let display = format!("{result}");
assert!(!display.is_empty());
}
}
#[test]
fn test_emulation_outcome_completed() {
let outcome = EmulationOutcome::Completed {
return_value: Some(EmValue::I32(100)),
instructions: 500,
};
assert!(outcome.is_completed());
assert_eq!(outcome.return_value(), Some(&EmValue::I32(100)));
assert_eq!(outcome.instructions_executed(), 500);
}
#[test]
fn test_emulation_outcome_exception() {
let outcome = EmulationOutcome::UnhandledException {
exception: EmValue::Null,
instructions: 250,
};
assert!(!outcome.is_completed());
assert_eq!(outcome.instructions_executed(), 250);
}
#[test]
fn test_emulation_outcome_limit() {
let outcome = EmulationOutcome::LimitReached {
limit: LimitExceeded::Timeout {
elapsed: Duration::from_secs(10),
limit: Duration::from_secs(5),
},
partial_state: None,
};
assert!(!outcome.is_completed());
let display = format!("{outcome}");
assert!(display.contains("limit"));
}
#[test]
fn test_emulation_outcome_display() {
let outcome = EmulationOutcome::Completed {
return_value: Some(EmValue::I32(42)),
instructions: 100,
};
let display = format!("{outcome}");
assert!(display.contains("completed"));
assert!(display.contains("100"));
}
}