diff --git a/crates/noxis-passes/src/rename/assign.rs b/crates/noxis-passes/src/rename/assign.rs index d9d2e44..77c12ce 100644 --- a/crates/noxis-passes/src/rename/assign.rs +++ b/crates/noxis-passes/src/rename/assign.rs @@ -196,6 +196,11 @@ pub(super) fn generate_packages(cfg: &Config, rng: &mut Rng) -> Vec { } /// Rewrite a descriptor/signature: `Lpkg/Class;` → the new class name. +/// +/// Generic signatures nest (`LMap;`): the name scan stops at +/// `<` as well as `;`, so type arguments fall back into the main loop and +/// get rewritten in turn. Stopping only at `;` swallowed the inner +/// reference into one unmapped blob (a stale `Comparable` leak). pub(super) fn rewrite_descriptor(desc: &str, classes: &BTreeMap) -> String { if !desc.contains('L') { return desc.to_string(); @@ -208,15 +213,17 @@ pub(super) fn rewrite_descriptor(desc: &str, classes: &BTreeMap) continue; } let mut name = String::new(); + let mut term = ';'; for c in chars.by_ref() { - if c == ';' { + if c == ';' || c == '<' { + term = c; break; } name.push(c); } out.push('L'); out.push_str(classes.get(&name).map_or(name.as_str(), String::as_str)); - out.push(';'); + out.push(term); } out } diff --git a/crates/noxis-passes/src/rename/tests.rs b/crates/noxis-passes/src/rename/tests/mod.rs similarity index 72% rename from crates/noxis-passes/src/rename/tests.rs rename to crates/noxis-passes/src/rename/tests/mod.rs index 93a411f..5cc4a41 100644 --- a/crates/noxis-passes/src/rename/tests.rs +++ b/crates/noxis-passes/src/rename/tests/mod.rs @@ -13,6 +13,8 @@ use noxis_core::{Config, Context, Pass, RenameConfig}; use super::RenamePass; use super::assign::{RenameMaps, rewrite_descriptor}; +mod rewrite; + fn sample() -> Program { let main = ClassBuilder::new("app/Main", None) .method_with_code( @@ -129,6 +131,14 @@ fn descriptor_rewrite_handles_arrays() { "(Lp/Q;[Lp/R;)Lp/Q;" ); assert_eq!(rewrite_descriptor("([[I)V", &map), "([[I)V"); + map.insert("app/Helper".to_string(), "p/H".to_string()); + assert_eq!( + rewrite_descriptor( + "Ljava/lang/Object;Ljava/lang/Comparable;", + &map + ), + "Ljava/lang/Object;Ljava/lang/Comparable;" + ); } #[test] @@ -191,77 +201,3 @@ fn subclass_gets_alias_for_inherited_member() { // Чужого члена не выдумываем. assert_eq!(maps.method("p/Sub", "other", "()V"), None); } - -/// Array type operands (`checkcast`, `instanceof`, `anewarray`, -/// `multianewarray`) carry descriptors, not plain names: the element class -/// inside must follow the mapping, or the reference silently goes stale -/// (and the next frame computation merges against a ghost type). -#[test] -fn array_type_operands_follow_their_element() { - let mut program = sample(); - let main = program - .classes - .iter_mut() - .find(|c| c.this_class == "app/Main") - .expect("main"); - let probe = main - .methods - .iter_mut() - .find(|m| m.name == "main") - .expect("main method"); - let code = probe.code.as_mut().expect("code"); - code.insns.extend([ - Insn::CheckCast("[Lapp/Helper;".to_string()), - Insn::Pop, - Insn::InstanceOf("[Lapp/Helper;".to_string()), - Insn::Pop, - Insn::ANewArray("app/Helper".to_string()), - Insn::Pop, - Insn::MultiANewArray { - descriptor: "[[Lapp/Helper;".to_string(), - dims: 2, - }, - Insn::Pop, - ]); - let ctx = run_rename(&mut program); - let helper = ctx.mapping.class("app/Helper").expect("renamed"); - let main = program.class("app/Main").expect("kept"); - let method = main - .methods - .iter() - .find(|m| m.name == "main") - .expect("main"); - let insns = &method.code.as_ref().expect("code").insns; - let cast = insns - .iter() - .find_map(|i| match i { - Insn::CheckCast(name) => Some(name), - _ => None, - }) - .expect("checkcast"); - assert_eq!(cast, &format!("[L{helper};")); - let inst = insns - .iter() - .find_map(|i| match i { - Insn::InstanceOf(name) => Some(name), - _ => None, - }) - .expect("instanceof"); - assert_eq!(inst, &format!("[L{helper};")); - let anew = insns - .iter() - .find_map(|i| match i { - Insn::ANewArray(name) => Some(name), - _ => None, - }) - .expect("anewarray"); - assert_eq!(anew, helper); - let multi = insns - .iter() - .find_map(|i| match i { - Insn::MultiANewArray { descriptor, .. } => Some(descriptor), - _ => None, - }) - .expect("multianewarray"); - assert_eq!(multi, &format!("[[L{helper};")); -} diff --git a/crates/noxis-passes/src/rename/tests/rewrite.rs b/crates/noxis-passes/src/rename/tests/rewrite.rs new file mode 100644 index 0000000..48c6af5 --- /dev/null +++ b/crates/noxis-passes/src/rename/tests/rewrite.rs @@ -0,0 +1,111 @@ +//! Reference rewriting: array operands and generic signatures. + +#![allow(clippy::expect_used, clippy::unwrap_used)] + +use super::{run_rename, sample}; +use noxis_classfile::Insn; +/// Array type operands (`checkcast`, `instanceof`, `anewarray`, +/// `multianewarray`) carry descriptors, not plain names: the element class +/// inside must follow the mapping, or the reference silently goes stale +/// (and the next frame computation merges against a ghost type). +#[test] +fn array_type_operands_follow_their_element() { + let mut program = sample(); + let main = program + .classes + .iter_mut() + .find(|c| c.this_class == "app/Main") + .expect("main"); + let probe = main + .methods + .iter_mut() + .find(|m| m.name == "main") + .expect("main method"); + let code = probe.code.as_mut().expect("code"); + code.insns.extend([ + Insn::CheckCast("[Lapp/Helper;".to_string()), + Insn::Pop, + Insn::InstanceOf("[Lapp/Helper;".to_string()), + Insn::Pop, + Insn::ANewArray("app/Helper".to_string()), + Insn::Pop, + Insn::MultiANewArray { + descriptor: "[[Lapp/Helper;".to_string(), + dims: 2, + }, + Insn::Pop, + ]); + let ctx = run_rename(&mut program); + let helper = ctx.mapping.class("app/Helper").expect("renamed"); + let main = program.class("app/Main").expect("kept"); + let method = main + .methods + .iter() + .find(|m| m.name == "main") + .expect("main"); + let insns = &method.code.as_ref().expect("code").insns; + let cast = insns + .iter() + .find_map(|i| match i { + Insn::CheckCast(name) => Some(name), + _ => None, + }) + .expect("checkcast"); + assert_eq!(cast, &format!("[L{helper};")); + let inst = insns + .iter() + .find_map(|i| match i { + Insn::InstanceOf(name) => Some(name), + _ => None, + }) + .expect("instanceof"); + assert_eq!(inst, &format!("[L{helper};")); + let anew = insns + .iter() + .find_map(|i| match i { + Insn::ANewArray(name) => Some(name), + _ => None, + }) + .expect("anewarray"); + assert_eq!(anew, helper); + let multi = insns + .iter() + .find_map(|i| match i { + Insn::MultiANewArray { descriptor, .. } => Some(descriptor), + _ => None, + }) + .expect("multianewarray"); + assert_eq!(multi, &format!("[[L{helper};")); +} + +/// Generic `Signature`s rewrite every referenced class, not just the first: +/// a stale `Comparable` leaks the original name and breaks reflective +/// generic resolution at runtime. +#[test] +fn generic_signatures_rewrite_all_references() { + use noxis_classfile::Attribute; + let mut program = sample(); + let main = program + .classes + .iter_mut() + .find(|c| c.this_class == "app/Main") + .expect("main"); + main.attributes.push(Attribute::Signature( + "Ljava/lang/Object;Ljava/lang/Comparable;".to_string(), + )); + let ctx = run_rename(&mut program); + let helper = ctx.mapping.class("app/Helper").expect("renamed"); + let main = program.class("app/Main").expect("kept"); + let signature = main + .attributes + .iter() + .find_map(|attr| match attr { + Attribute::Signature(text) => Some(text), + _ => None, + }) + .expect("signature"); + assert_eq!( + signature, + &format!("Ljava/lang/Object;Ljava/lang/Comparable;") + ); +}