obfuskator/crates/noxis-classfile/tests/roundtrip/main.rs
loki5512344 15cd31252e
classfile,passes: model RuntimeVisibleTypeAnnotations and AnnotationDefault
Type annotations (method- and code-level) and annotation defaults were
passed through as Raw bytes with the original pool indices; the writer
rebuilds the pool, so those indices pointed at the wrong utf8 or past
the pool end. HotSpot tolerates the garbage (lazy reflection parsing),
but ASM's ClassReader dies with ArrayIndexOutOfBoundsException while
Paper's plugin converter reads every method - 76 of 275 LoParkour
classes failed to convert. Target_info and type_path carry no pool
references and stay opaque bytes; type descriptors and element values
are modeled and rewritten by rename like plain annotations.
2026-10-13 10:17:10 +02:00

173 lines
6.3 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

//! Round-trip `parse → write → parse` через backend.
//!
//! Пул при записи строится заново (порядок другой), поэтому сравнивается IR,
//! а не байты. Записанные байты обязаны проходить структурный `verify`
//! backend'а - это ловит битые ссылки пула и кривые атрибуты.
#![allow(clippy::expect_used, clippy::unwrap_used)]
use noxis_classfile::{ClassFile, format};
use ristretto_classfile::attributes::{
Attribute as RtAttr, ExceptionTableEntry as RtEntry, Instruction as Rt, LineNumber as RtLine,
StackFrame as RtFrame,
};
use ristretto_classfile::{
ClassAccessFlags, ClassFile as RtClass, ConstantPool, Field as RtField, FieldAccessFlags,
FieldType, JAVA_21, Method as RtMethod, MethodAccessFlags,
};
/// Минимальный класс: версия, флаги, поле с `ConstantValue`, `SourceFile`.
#[test]
fn minimal_class_round_trip() {
let mut pool = ConstantPool::default();
let this = pool.add_class("demo/Round").expect("add_class");
let object = pool.add_class("java/lang/Object").expect("add_object");
let name = pool.add_utf8("K").expect("name");
let desc = pool.add_utf8("I").expect("desc");
let value = pool.add_integer(42).expect("value");
let source_name = pool.add_utf8("SourceFile").expect("attr");
let source_file = pool.add_utf8("Round.java").expect("file");
let constant_name = pool.add_utf8("ConstantValue").expect("attr");
let rt = RtClass {
version: JAVA_21,
constant_pool: pool,
access_flags: ClassAccessFlags::PUBLIC,
this_class: this,
super_class: object,
interfaces: vec![],
fields: vec![RtField {
access_flags: FieldAccessFlags::PUBLIC
| FieldAccessFlags::STATIC
| FieldAccessFlags::FINAL,
name_index: name,
descriptor_index: desc,
field_type: FieldType::parse("I").expect("field type"),
attributes: vec![RtAttr::ConstantValue {
name_index: constant_name,
constant_value_index: value,
}],
}],
methods: vec![],
attributes: vec![RtAttr::SourceFile {
name_index: source_name,
source_file_index: source_file,
}],
..RtClass::default()
};
let ir = round_trip(&rt);
assert_eq!(ir.this_class, "demo/Round");
assert_eq!(ir.super_class.as_deref(), Some("java/lang/Object"));
assert_eq!(ir.methods.len(), 0);
assert_eq!(ir.fields.len(), 1);
}
/// Метод с ветвлениями, handler'ом, номерами строк и кадрами.
#[test]
fn code_with_branches_round_trip() {
let mut pool = ConstantPool::default();
let this = pool.add_class("demo/Flow").expect("add_class");
let object = pool.add_class("java/lang/Object").expect("add_object");
let name = pool.add_utf8("run").expect("name");
let desc = pool.add_utf8("()I").expect("desc");
let code_name = pool.add_utf8("Code").expect("attr");
let lines_name = pool.add_utf8("LineNumberTable").expect("attr");
let frames_name = pool.add_utf8("StackMapTable").expect("attr");
// Логические номера: 0 iconst_0, 1 istore_1, 2 iload_1, 3 ifeq→6,
// 4 iconst_1, 5 goto→7, 6 iconst_2, 7 ireturn.
let code = vec![
Rt::Iconst_0,
Rt::Istore_1,
Rt::Iload_1,
Rt::Ifeq(6),
Rt::Iconst_1,
Rt::Goto(7),
Rt::Iconst_2,
Rt::Ireturn,
];
let code_attr = RtAttr::Code {
name_index: code_name,
max_stack: 1,
max_locals: 2,
code,
exception_table: vec![RtEntry {
range_pc: 0..4,
handler_pc: 6,
catch_type: 0,
}],
attributes: vec![
RtAttr::LineNumberTable {
name_index: lines_name,
line_numbers: vec![
RtLine {
start_pc: 0,
line_number: 10,
},
RtLine {
start_pc: 6,
line_number: 20,
},
],
},
RtAttr::StackMapTable {
name_index: frames_name,
frames: vec![
RtFrame::SameFrame { frame_type: 6 },
RtFrame::SameFrame { frame_type: 0 },
],
},
],
};
let rt = RtClass {
version: JAVA_21,
constant_pool: pool,
access_flags: ClassAccessFlags::PUBLIC,
this_class: this,
super_class: object,
interfaces: vec![],
fields: vec![],
methods: vec![RtMethod {
access_flags: MethodAccessFlags::PUBLIC | MethodAccessFlags::STATIC,
name_index: name,
descriptor_index: desc,
attributes: vec![code_attr],
}],
attributes: vec![],
..RtClass::default()
};
let ir = round_trip(&rt);
let method = &ir.methods[0];
let code = method.code.as_ref().expect("есть Code");
assert_eq!(code.max_stack, 1);
assert_eq!(code.max_locals, 2);
// Номера инструкций: 0,1,2,3(ifeq),4,5(goto),6,7; кадры на 6 и 7.
let frames = &code.frames;
assert_eq!(frames.len(), 2);
assert!(format!("{frames:?}").contains("offset_delta: 6"));
assert_eq!(code.exception_table.len(), 1);
assert_eq!(code.attributes.len(), 1);
}
fn round_trip(rt: &RtClass<'_>) -> ClassFile {
let mut original = Vec::new();
rt.to_bytes(&mut original).expect("to_bytes");
let first = format::parse(&original).expect("parse");
let written = format::write(&first).expect("write");
RtClass::from_bytes(&written)
.expect("written parses")
.verify()
.expect("written verifies");
let second = format::parse(&written).expect("re-parse");
assert_eq!(first, second, "IR обязан пережить round-trip");
second
}
/// A bare `0x200` interface (no `ABSTRACT`, as some compilers emit) must
/// survive the round trip: HotSpot tolerates it, so we normalize on parse
/// instead of dying in the backend's strict verifier on write.
mod attrs;
mod code;
mod type_annotations;