use std::collections::HashSet;
use dashmap::{DashMap, DashSet};
use crate::{analysis::ConstValue, metadata::token::Token};
#[derive(Debug, Default)]
pub struct DecryptorContext {
registered: DashSet<Token>,
decrypted: DashMap<Token, boxcar::Vec<DecryptedCall>>,
failed: DashMap<Token, boxcar::Vec<FailedCall>>,
cache: DashMap<CacheKey, ConstValue>,
methodspec_to_decryptor: DashMap<Token, Token>,
}
#[derive(Debug, Clone)]
pub struct DecryptedCall {
pub caller: Token,
pub location: usize,
pub value: ConstValue,
}
#[derive(Debug, Clone)]
pub struct FailedCall {
pub caller: Token,
pub location: usize,
pub reason: FailureReason,
}
#[derive(Debug, Clone)]
pub enum FailureReason {
NonConstantArgs,
EmulationFailed(String),
UnresolvedTarget,
InvalidReturnValue,
MethodNotFound,
}
impl FailureReason {
#[must_use]
pub fn is_permanent(&self) -> bool {
!matches!(self, Self::NonConstantArgs)
}
}
impl std::fmt::Display for FailureReason {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::NonConstantArgs => write!(f, "arguments not constant"),
Self::EmulationFailed(msg) => write!(f, "emulation failed: {msg}"),
Self::UnresolvedTarget => write!(f, "unresolved call target"),
Self::InvalidReturnValue => write!(f, "invalid return value"),
Self::MethodNotFound => write!(f, "method not found"),
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub struct CacheKey {
pub decryptor: Token,
pub args_repr: String,
}
impl CacheKey {
#[must_use]
pub fn new(decryptor: Token, args: &[ConstValue]) -> Self {
Self {
decryptor,
args_repr: Self::args_to_string(args),
}
}
fn args_to_string(args: &[ConstValue]) -> String {
args.iter()
.map(|arg| format!("{arg:?}"))
.collect::<Vec<_>>()
.join(",")
}
}
impl DecryptorContext {
#[must_use]
pub fn new() -> Self {
Self::default()
}
pub fn register(&self, token: Token) {
self.registered.insert(token);
}
pub fn register_many(&self, tokens: impl IntoIterator<Item = Token>) {
for token in tokens {
self.register(token);
}
}
#[must_use]
pub fn unregister(&self, token: Token) -> bool {
self.registered.remove(&token).is_some()
}
#[must_use]
pub fn registered_tokens(&self) -> HashSet<Token> {
self.registered.iter().map(|r| *r).collect()
}
pub fn map_methodspec(&self, methodspec: Token, decryptor: Token) {
self.methodspec_to_decryptor.insert(methodspec, decryptor);
}
pub fn map_methodspecs(&self, methodspecs: impl IntoIterator<Item = Token>, decryptor: Token) {
for ms in methodspecs {
self.map_methodspec(ms, decryptor);
}
}
#[must_use]
pub fn resolve_decryptor(&self, target: Token) -> Option<Token> {
if self.registered.contains(&target) {
return Some(target);
}
if let Some(decryptor_ref) = self.methodspec_to_decryptor.get(&target) {
let decryptor = *decryptor_ref;
if self.registered.contains(&decryptor) {
return Some(decryptor);
}
}
None
}
#[must_use]
pub fn is_decryptor(&self, token: Token) -> bool {
self.resolve_decryptor(token).is_some()
}
#[must_use]
pub fn all_decryptors(&self) -> Vec<Token> {
self.registered.iter().map(|r| *r).collect()
}
#[must_use]
pub fn decryptor_count(&self) -> usize {
self.registered.len()
}
#[must_use]
pub fn has_decryptors(&self) -> bool {
!self.registered.is_empty()
}
#[must_use]
pub fn methodspec_mapping_count(&self) -> usize {
self.methodspec_to_decryptor.len()
}
pub fn all_methodspec_mappings(&self) -> impl Iterator<Item = (Token, Token)> + '_ {
self.methodspec_to_decryptor
.iter()
.map(|r| (*r.key(), *r.value()))
}
pub fn with_cached<R, F>(&self, decryptor: Token, args: &[ConstValue], f: F) -> Option<R>
where
F: FnOnce(&ConstValue) -> R,
{
let key = CacheKey::new(decryptor, args);
self.cache.get(&key).map(|r| f(&r))
}
pub fn cache_value(&self, decryptor: Token, args: &[ConstValue], value: ConstValue) {
let key = CacheKey::new(decryptor, args);
self.cache.insert(key, value);
}
#[must_use]
pub fn is_cached(&self, decryptor: Token, args: &[ConstValue]) -> bool {
let key = CacheKey::new(decryptor, args);
self.cache.contains_key(&key)
}
pub fn clear_cache(&self) {
self.cache.clear();
}
pub fn record_success(
&self,
decryptor: Token,
caller: Token,
location: usize,
value: ConstValue,
) {
self.decrypted
.entry(decryptor)
.or_default()
.push(DecryptedCall {
caller,
location,
value,
});
}
pub fn record_failure(
&self,
decryptor: Token,
caller: Token,
location: usize,
reason: FailureReason,
) {
self.failed.entry(decryptor).or_default().push(FailedCall {
caller,
location,
reason,
});
}
#[must_use]
pub fn get_decrypted(&self, decryptor: Token) -> Option<Vec<DecryptedCall>> {
self.decrypted
.get(&decryptor)
.map(|r| r.iter().map(|(_, v)| v.clone()).collect())
}
#[must_use]
pub fn get_failed(&self, decryptor: Token) -> Option<Vec<FailedCall>> {
self.failed
.get(&decryptor)
.map(|r| r.iter().map(|(_, v)| v.clone()).collect())
}
#[must_use]
pub fn has_permanent_failure(&self, caller: Token, location: usize) -> bool {
for entry in &self.failed {
for (_, failure) in entry.value() {
if failure.caller == caller
&& failure.location == location
&& failure.reason.is_permanent()
{
return true;
}
}
}
false
}
#[must_use]
pub fn is_already_decrypted(&self, caller: Token, location: usize) -> bool {
for entry in &self.decrypted {
for (_, call) in entry.value() {
if call.caller == caller && call.location == location {
return true;
}
}
}
false
}
#[must_use]
pub fn total_decrypted(&self) -> usize {
self.decrypted
.iter()
.map(|entry| entry.value().count())
.sum()
}
#[must_use]
pub fn total_failed(&self) -> usize {
self.failed.iter().map(|entry| entry.value().count()).sum()
}
#[must_use]
pub fn is_fully_decrypted(&self, decryptor: Token) -> bool {
self.failed.get(&decryptor).is_none_or(|r| r.count() == 0)
}
#[must_use]
pub fn removable_decryptors(&self) -> Vec<Token> {
self.registered
.iter()
.map(|r| *r.key())
.filter(|t| self.is_fully_decrypted(*t))
.collect()
}
#[must_use]
pub fn active_decryptors(&self) -> Vec<Token> {
self.registered
.iter()
.map(|r| *r.key())
.filter(|t| self.decrypted.get(t).is_some_and(|r| r.count() > 0))
.collect()
}
#[must_use]
pub fn problematic_decryptors(&self) -> Vec<Token> {
self.registered
.iter()
.map(|r| *r.key())
.filter(|t| self.failed.get(t).is_some_and(|r| r.count() > 0))
.collect()
}
pub fn clear_results(&self) {
self.decrypted.clear();
self.failed.clear();
self.cache.clear();
}
pub fn clear_all(&mut self) {
self.registered.clear();
self.decrypted.clear();
self.failed.clear();
self.cache.clear();
self.methodspec_to_decryptor.clear();
}
}
#[cfg(test)]
mod tests {
use std::sync::Arc;
use std::thread;
use super::*;
#[test]
fn test_register_and_query() {
let ctx = DecryptorContext::new();
let token = Token::new(0x06000001);
assert!(!ctx.is_decryptor(token));
ctx.register(token);
assert!(ctx.is_decryptor(token));
}
#[test]
fn test_register_many() {
let ctx = DecryptorContext::new();
let tokens = vec![
Token::new(0x06000001),
Token::new(0x06000002),
Token::new(0x06000003),
];
ctx.register_many(tokens.iter().copied());
assert_eq!(ctx.decryptor_count(), 3);
for token in &tokens {
assert!(ctx.is_decryptor(*token));
}
}
#[test]
fn test_methodspec_resolution() {
let ctx = DecryptorContext::new();
let decryptor = Token::new(0x06000001);
let methodspec1 = Token::new(0x2b000001);
let methodspec2 = Token::new(0x2b000002);
ctx.register(decryptor);
ctx.map_methodspec(methodspec1, decryptor);
ctx.map_methodspec(methodspec2, decryptor);
assert!(ctx.is_decryptor(methodspec1));
assert!(ctx.is_decryptor(methodspec2));
assert_eq!(ctx.resolve_decryptor(methodspec1), Some(decryptor));
assert_eq!(ctx.resolve_decryptor(methodspec2), Some(decryptor));
}
#[test]
fn test_caching() {
let ctx = DecryptorContext::new();
let decryptor = Token::new(0x06000001);
let args = vec![ConstValue::I32(42)];
let value = ConstValue::I32(100);
assert!(!ctx.is_cached(decryptor, &args));
ctx.cache_value(decryptor, &args, value.clone());
assert!(ctx.is_cached(decryptor, &args));
assert!(ctx
.with_cached(decryptor, &args, |v| *v == value)
.unwrap_or(false));
let other_args = vec![ConstValue::I32(43)];
assert!(!ctx.is_cached(decryptor, &other_args));
}
#[test]
fn test_result_tracking() {
let ctx = DecryptorContext::new();
let decryptor = Token::new(0x06000001);
let caller = Token::new(0x06000002);
ctx.register(decryptor);
ctx.record_success(decryptor, caller, 100, ConstValue::I32(42));
ctx.record_success(decryptor, caller, 200, ConstValue::I32(43));
assert_eq!(ctx.total_decrypted(), 2);
assert_eq!(ctx.total_failed(), 0);
assert!(ctx.is_fully_decrypted(decryptor));
ctx.record_failure(decryptor, caller, 300, FailureReason::NonConstantArgs);
assert_eq!(ctx.total_decrypted(), 2);
assert_eq!(ctx.total_failed(), 1);
assert!(!ctx.is_fully_decrypted(decryptor));
}
#[test]
fn test_removable_decryptors() {
let ctx = DecryptorContext::new();
let decryptor1 = Token::new(0x06000001);
let decryptor2 = Token::new(0x06000002);
let caller = Token::new(0x06000003);
ctx.register(decryptor1);
ctx.register(decryptor2);
ctx.record_success(decryptor1, caller, 100, ConstValue::I32(1));
ctx.record_success(decryptor2, caller, 200, ConstValue::I32(2));
ctx.record_failure(decryptor2, caller, 300, FailureReason::NonConstantArgs);
let removable = ctx.removable_decryptors();
assert!(removable.contains(&decryptor1));
assert!(!removable.contains(&decryptor2));
}
#[test]
fn test_stats() {
let ctx = DecryptorContext::new();
let decryptor = Token::new(0x06000001);
let caller = Token::new(0x06000002);
let methodspec = Token::new(0x2b000001);
ctx.register(decryptor);
ctx.map_methodspec(methodspec, decryptor);
ctx.cache_value(decryptor, &[ConstValue::I32(1)], ConstValue::I32(10));
ctx.record_success(decryptor, caller, 100, ConstValue::I32(10));
ctx.record_failure(decryptor, caller, 200, FailureReason::NonConstantArgs);
assert!(ctx.is_decryptor(decryptor));
assert!(ctx.has_decryptors());
assert_eq!(ctx.resolve_decryptor(methodspec), Some(decryptor));
assert!(ctx.is_already_decrypted(caller, 100));
assert!(!ctx.is_already_decrypted(caller, 200));
assert_eq!(ctx.total_decrypted(), 1);
assert_eq!(ctx.total_failed(), 1);
}
#[test]
fn test_unregister() {
let ctx = DecryptorContext::new();
let token = Token::new(0x06000001);
ctx.register(token);
assert!(ctx.is_decryptor(token));
let removed = ctx.unregister(token);
assert!(removed);
assert!(!ctx.is_decryptor(token));
assert!(!ctx.unregister(token));
}
#[test]
fn test_clear_results() {
let ctx = DecryptorContext::new();
let decryptor = Token::new(0x06000001);
let caller = Token::new(0x06000002);
ctx.register(decryptor);
ctx.cache_value(decryptor, &[ConstValue::I32(1)], ConstValue::I32(10));
ctx.record_success(decryptor, caller, 100, ConstValue::I32(10));
ctx.clear_results();
assert!(ctx.is_decryptor(decryptor));
assert_eq!(ctx.total_decrypted(), 0);
assert!(!ctx.is_cached(decryptor, &[ConstValue::I32(1)]));
}
#[test]
fn test_cache_key_equality() {
let key1 = CacheKey::new(Token::new(0x06000001), &[ConstValue::I32(42)]);
let key2 = CacheKey::new(Token::new(0x06000001), &[ConstValue::I32(42)]);
let key3 = CacheKey::new(Token::new(0x06000001), &[ConstValue::I32(43)]);
let key4 = CacheKey::new(Token::new(0x06000002), &[ConstValue::I32(42)]);
assert_eq!(key1, key2);
assert_ne!(key1, key3);
assert_ne!(key1, key4);
}
#[test]
fn test_failure_reason_display() {
assert_eq!(
FailureReason::NonConstantArgs.to_string(),
"arguments not constant"
);
assert_eq!(
FailureReason::EmulationFailed("timeout".to_string()).to_string(),
"emulation failed: timeout"
);
}
#[test]
fn test_failure_reason_is_permanent() {
assert!(!FailureReason::NonConstantArgs.is_permanent());
assert!(FailureReason::EmulationFailed("timeout".to_string()).is_permanent());
assert!(FailureReason::UnresolvedTarget.is_permanent());
assert!(FailureReason::InvalidReturnValue.is_permanent());
assert!(FailureReason::MethodNotFound.is_permanent());
}
#[test]
fn test_has_permanent_failure() {
let ctx = DecryptorContext::new();
let decryptor = Token::new(0x06000001);
let caller = Token::new(0x06000002);
ctx.register(decryptor);
assert!(!ctx.has_permanent_failure(caller, 100));
ctx.record_failure(decryptor, caller, 100, FailureReason::NonConstantArgs);
assert!(!ctx.has_permanent_failure(caller, 100));
ctx.record_failure(
decryptor,
caller,
200,
FailureReason::EmulationFailed("timeout".to_string()),
);
assert!(!ctx.has_permanent_failure(caller, 100));
assert!(ctx.has_permanent_failure(caller, 200));
}
#[test]
fn test_is_already_decrypted() {
let ctx = DecryptorContext::new();
let decryptor = Token::new(0x06000001);
let caller = Token::new(0x06000002);
ctx.register(decryptor);
assert!(!ctx.is_already_decrypted(caller, 100));
ctx.record_success(decryptor, caller, 100, ConstValue::I32(42));
assert!(ctx.is_already_decrypted(caller, 100));
assert!(!ctx.is_already_decrypted(caller, 200));
assert!(!ctx.is_already_decrypted(Token::new(0x06000003), 100));
}
#[test]
fn test_thread_safe_access() {
let ctx = Arc::new(DecryptorContext::new());
let mut handles = vec![];
for i in 0..4 {
let ctx_clone = Arc::clone(&ctx);
handles.push(thread::spawn(move || {
for j in 0..50_i32 {
let decryptor = Token::new(0x06000000 + (i * 50 + j) as u32);
ctx_clone.register(decryptor);
ctx_clone.record_success(
decryptor,
Token::new(0x06001000),
j as usize,
ConstValue::I32(j),
);
ctx_clone.cache_value(decryptor, &[ConstValue::I32(j)], ConstValue::I32(j * 2));
}
}));
}
for handle in handles {
handle.join().unwrap();
}
assert_eq!(ctx.decryptor_count(), 200);
assert_eq!(ctx.total_decrypted(), 200);
}
}