diff --git a/dexdec/src/analysis/kotlin_backend/kotlin_model/declared_members.rs b/dexdec/src/analysis/kotlin_backend/kotlin_model/declared_members.rs index 084baec..e10a386 100644 --- a/dexdec/src/analysis/kotlin_backend/kotlin_model/declared_members.rs +++ b/dexdec/src/analysis/kotlin_backend/kotlin_model/declared_members.rs @@ -10,8 +10,9 @@ use std::str::FromStr; use super::metadata_members::{backing_field_references, MetadataCallable}; use crate::frontend::kotlin_metadata::{KotlinMetadata, TypeReference, Visibility}; -use crate::frontend::ClassNode; -use crate::ir::{FieldReference, MethodDescriptor, MethodReference}; +use crate::frontend::{ClassNode, MethodNode}; +use crate::ir::generic_types::{GenericSignatures, JvmTypeSignature, TypeArgument}; +use crate::ir::{ArgType, FieldReference, MethodDescriptor, MethodReference}; use crate::language::kotlin::{KotlinDefaultCallContract, KotlinDefaultMask, KotlinIdentifier}; use std::sync::Arc; @@ -571,6 +572,7 @@ impl KotlinDeclaredMembers { singletons: &mut std::collections::BTreeSet, ) { let Some(Ok(metadata)) = KotlinMetadata::of(&class.annotations) else { + self.collect_suspend_abi(class); return; }; let declarations = metadata.declarations(); @@ -726,6 +728,32 @@ impl KotlinDeclaredMembers { self.fields.insert(reference, property.flags.visibility()); } } + self.collect_suspend_abi(class); + } + + fn collect_suspend_abi(&mut self, class: &ClassNode) { + let continuation_impl = class + .methods() + .iter() + .any(|method| method.name() == "invokeSuspend"); + for method in class.methods() { + if method.is_constructor() || method.name() == "" { + continue; + } + if method.name() == "create" || method.name() == "invokeSuspend" { + continue; + } + if continuation_impl && method.name() == "invoke" { + continue; + } + let Some(declaration) = suspend_abi_declaration(method) else { + continue; + }; + let reference = method_reference(class, method); + self.suspend_functions + .entry(reference) + .or_insert(declaration); + } } fn index_default_calls( @@ -931,6 +959,73 @@ impl KotlinDeclaredMembers { } } +fn method_reference(class: &ClassNode, method: &MethodNode) -> MethodReference { + MethodReference { + owner: class.class_type().clone(), + name: method.name().to_string(), + descriptor: MethodDescriptor { + parameters: method.param_types().to_vec(), + return_type: method.return_type().clone(), + }, + } +} + +fn suspend_abi_declaration(method: &MethodNode) -> Option { + if method.return_type() != &ArgType::object("java/lang/Object") { + return None; + } + let continuation_parameter = method.param_types().len().checked_sub(1)?; + if !is_continuation_type(&method.param_types()[continuation_parameter]) { + return None; + } + Some(KotlinSuspendDeclaration { + continuation_parameter, + return_type: suspend_abi_return_type(method), + }) +} + +fn suspend_abi_return_type(method: &MethodNode) -> ArgType { + continuation_result_type(method).unwrap_or_else(|| ArgType::object("java/lang/Object")) +} + +fn continuation_result_type(method: &MethodNode) -> Option { + let signature = method + .signature + .as_deref() + .and_then(|signature| GenericSignatures::method(signature).ok()) + .or_else(|| { + method + .override_semantics + .as_ref() + .and_then(|semantics| semantics.inherited_signature.clone()) + })?; + let last = signature.parameter_types.last()?; + continuation_type_argument(last).map(|ty| ty.erased()) +} + +fn continuation_type_argument(ty: &JvmTypeSignature) -> Option<&JvmTypeSignature> { + let JvmTypeSignature::ClassType(class) = ty else { + return None; + }; + if !is_continuation_class_name(&class.erased_name()) { + return None; + } + match class.type_arguments.first()? { + TypeArgument::Super(inner) | TypeArgument::Exact(inner) | TypeArgument::Extends(inner) => { + Some(inner) + } + TypeArgument::Unbounded => None, + } +} + +fn is_continuation_type(ty: &ArgType) -> bool { + ty.as_object().is_some_and(is_continuation_class_name) +} + +fn is_continuation_class_name(name: &str) -> bool { + name == "kotlin/coroutines/Continuation" || name.ends_with("/Continuation") +} + fn property_accessor_reference( class: &ClassNode, signature: &crate::frontend::kotlin_metadata::JvmSignature, @@ -941,3 +1036,112 @@ fn property_accessor_reference( descriptor: MethodDescriptor::from_str(&signature.descriptor).ok()?, }) } + +#[cfg(test)] +mod tests { + use super::*; + use crate::frontend::{AccessInfo, ClassInfo, MethodInfo}; + + fn class_with_methods(name: &str, methods: Vec) -> ClassNode { + let mut class = ClassNode::new( + 0, + ClassInfo::from_type_descriptor(&format!("L{name};")).expect("class descriptor"), + AccessInfo::for_class(0x1), + ); + for method in methods { + class.add_method(method); + } + class + } + + fn method(name: &str, parameters: Vec, return_type: ArgType) -> MethodNode { + MethodNode::new( + 0, + MethodInfo::new( + "Lsample/Host;".to_string(), + name.to_string(), + parameters, + return_type, + ), + AccessInfo::for_method(0x9), + ) + } + + #[test] + fn continuation_type_accepts_obfuscated_package() { + assert!(is_continuation_type(&ArgType::object( + "kotlin/coroutines/Continuation" + ))); + assert!(is_continuation_type(&ArgType::object("ef1/Continuation"))); + assert!(!is_continuation_type(&ArgType::object("java/lang/Object"))); + assert!(!is_continuation_type(&ArgType::INT)); + } + + #[test] + fn suspend_abi_reads_continuation_result_type() { + let method = method( + "await", + vec![ + ArgType::object("java/lang/String"), + ArgType::object("kotlin/coroutines/Continuation"), + ], + ArgType::object("java/lang/Object"), + ) + .with_signature(Some( + "(Ljava/lang/String;Lkotlin/coroutines/Continuation<-Ljava/lang/Integer;>;)Ljava/lang/Object;" + .into(), + )); + let declaration = suspend_abi_declaration(&method).expect("suspend ABI"); + assert_eq!(declaration.continuation_parameter, 1); + assert_eq!( + declaration.return_type, + ArgType::object("java/lang/Integer") + ); + } + + #[test] + fn collect_suspend_abi_skips_continuation_impl_invoke() { + let class = class_with_methods( + "sample/StateMachine", + vec![ + method( + "invokeSuspend", + vec![ArgType::object("java/lang/Object")], + ArgType::object("java/lang/Object"), + ), + method( + "invoke", + vec![ + ArgType::object("java/lang/String"), + ArgType::object("kotlin/coroutines/Continuation"), + ], + ArgType::object("java/lang/Object"), + ), + method( + "await", + vec![ArgType::object("kotlin/coroutines/Continuation")], + ArgType::object("java/lang/Object"), + ), + ], + ); + let declared = KotlinDeclaredMembers::analyze(&[&class], &|_, _| None); + let invoke = method_reference( + &class, + class + .methods() + .iter() + .find(|method| method.name() == "invoke") + .expect("invoke"), + ); + let await_method = method_reference( + &class, + class + .methods() + .iter() + .find(|method| method.name() == "await") + .expect("await"), + ); + assert!(declared.suspend_declaration(&invoke).is_none()); + assert!(declared.suspend_declaration(&await_method).is_some()); + } +} diff --git a/dexdec/src/api/decompiler.rs b/dexdec/src/api/decompiler.rs index f98124e..a9019e6 100644 --- a/dexdec/src/api/decompiler.rs +++ b/dexdec/src/api/decompiler.rs @@ -267,9 +267,10 @@ impl Decompiler { /// The language a class was written in, as far as the class itself says. /// /// The Kotlin compiler stamps everything it emits with `@kotlin.Metadata`, - /// down to the synthetic classes it makes for lambdas, and nothing else - /// writes that annotation. A class carrying none was not compiled from - /// Kotlin, so Java is what it should be read back as. + /// down to the synthetic classes it makes for lambdas. R8 often strips that + /// annotation but leaves the DEX `SourceFile` (`.kt`), which is the same + /// attribute JADX uses to recover Kotlin file names. A class with neither + /// signal is read back as Java. /// /// Only the class declaration is read; no method body is decoded. pub fn source_language( @@ -280,12 +281,10 @@ impl Decompiler { let Some(node) = self.context.load_class_deferred(&class)? else { return Err(DecompileError::ClassNotFound(class)); }; - let kotlin = node.annotations.iter().any(KotlinMetadata::is_metadata); - Ok(if kotlin { - SourceLanguage::Kotlin - } else { - SourceLanguage::Java - }) + Ok(inferred_source_language( + &node.annotations, + node.source_file.as_deref(), + )) } /// Inspect one class declaration without decoding any method body. @@ -582,6 +581,23 @@ pub fn kotlin_source_path(descriptor: &str) -> PathBuf { source_path(descriptor, SourceLanguage::Kotlin) } +fn inferred_source_language( + annotations: &[crate::frontend::AnnotationNode], + source_file: Option<&str>, +) -> SourceLanguage { + if annotations.iter().any(KotlinMetadata::is_metadata) || is_kotlin_source_file(source_file) { + SourceLanguage::Kotlin + } else { + SourceLanguage::Java + } +} + +fn is_kotlin_source_file(source_file: Option<&str>) -> bool { + source_file.is_some_and(|name| { + name.ends_with(".kt") || name.ends_with(".kts") || name.ends_with(".ktm") + }) +} + #[cfg(test)] mod tests { use super::*; @@ -601,4 +617,21 @@ mod tests { ClassSelector::Listed(BTreeSet::from(["LA;".to_string(), "LB;".to_string()])) ); } + + #[test] + fn source_file_kt_selects_kotlin_without_metadata() { + assert_eq!( + inferred_source_language(&[], Some("PipHintTracker.kt")), + SourceLanguage::Kotlin + ); + assert_eq!( + inferred_source_language(&[], Some("Script.kts")), + SourceLanguage::Kotlin + ); + assert_eq!( + inferred_source_language(&[], Some("Main.java")), + SourceLanguage::Java + ); + assert_eq!(inferred_source_language(&[], None), SourceLanguage::Java); + } }