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.
This commit is contained in:
Loki 2026-10-13 10:17:10 +02:00
parent 3f92b72c8b
commit 15cd31252e
Signed by: boba
GPG key ID: 253067914055423B
14 changed files with 654 additions and 224 deletions

View file

@ -0,0 +1,179 @@
//! Annotation attributes → IR: names instead of pool indices.
//!
//! Split out of `pool.rs`: these structures carry pool references that go
//! stale the moment the writer rebuilds the pool, so every one of them is
//! modeled (raw bytes made ASM die with `ArrayIndexOutOfBoundsException`).
use ristretto_classfile::ConstantPool;
use super::pool::{const_from, utf8};
use crate::error::ClassfileError;
use crate::insn::Const;
use crate::model::{Annotation, AnnotationValue, TypeAnnotation};
/// A list of `annotation` structures (`RuntimeVisibleAnnotations`).
pub(super) fn annos(
pool: &ConstantPool<'_>,
list: &[ristretto_classfile::attributes::Annotation],
) -> Result<Vec<Annotation>, ClassfileError> {
list.iter().map(|anno| annotation(pool, anno)).collect()
}
/// Per-parameter annotation lists (`RuntimeVisibleParameterAnnotations`).
pub(super) fn params(
pool: &ConstantPool<'_>,
list: &[ristretto_classfile::attributes::ParameterAnnotation],
) -> Result<Vec<Vec<Annotation>>, ClassfileError> {
list.iter()
.map(|param| {
param
.annotations
.iter()
.map(|anno| annotation(pool, anno))
.collect()
})
.collect()
}
/// A list of `type_annotation` structures (`RuntimeVisibleTypeAnnotations`).
pub(super) fn type_annos(
pool: &ConstantPool<'_>,
list: &[ristretto_classfile::attributes::TypeAnnotation],
) -> Result<Vec<TypeAnnotation>, ClassfileError> {
list.iter()
.map(|anno| type_annotation(pool, anno))
.collect()
}
/// One `element_value` → IR, keeping the tag (HotSpot checks it).
pub(super) fn element_value(
pool: &ConstantPool<'_>,
value: &ristretto_classfile::attributes::AnnotationElement,
) -> Result<AnnotationValue, ClassfileError> {
use ristretto_classfile::attributes::AnnotationElement as E;
Ok(match value {
E::Byte { const_value_index } => {
AnnotationValue::Byte(const_int(pool, *const_value_index)?)
}
E::Char { const_value_index } => {
AnnotationValue::Char(const_int(pool, *const_value_index)?)
}
E::Short { const_value_index } => {
AnnotationValue::Short(const_int(pool, *const_value_index)?)
}
E::Boolean { const_value_index } => {
AnnotationValue::Boolean(const_int(pool, *const_value_index)?)
}
E::Int { const_value_index } => AnnotationValue::Int(const_int(pool, *const_value_index)?),
E::Long { const_value_index } => {
AnnotationValue::Long(const_long(pool, *const_value_index)?)
}
E::Float { const_value_index } => {
AnnotationValue::Float(const_float(pool, *const_value_index)?)
}
E::Double { const_value_index } => {
AnnotationValue::Double(const_double(pool, *const_value_index)?)
}
E::String { const_value_index } => AnnotationValue::String(utf8(pool, *const_value_index)?),
E::Enum {
type_name_index,
const_name_index,
} => AnnotationValue::Enum {
descriptor: utf8(pool, *type_name_index)?,
name: utf8(pool, *const_name_index)?,
},
E::Class { class_info_index } => AnnotationValue::Class(utf8(pool, *class_info_index)?),
E::Annotation { annotation: nested } => {
AnnotationValue::Nested(Box::new(annotation(pool, nested)?))
}
E::Array { values } => AnnotationValue::Array(
values
.iter()
.map(|value| element_value(pool, value))
.collect::<Result<_, _>>()?,
),
})
}
/// One `annotation` structure → IR (names, not pool indices).
fn annotation(
pool: &ConstantPool<'_>,
anno: &ristretto_classfile::attributes::Annotation,
) -> Result<Annotation, ClassfileError> {
Ok(Annotation {
descriptor: utf8(pool, anno.type_index)?,
elements: anno
.elements
.iter()
.map(|pair| {
Ok((
utf8(pool, pair.name_index)?,
element_value(pool, &pair.value)?,
))
})
.collect::<Result<_, ClassfileError>>()?,
})
}
/// One `type_annotation` structure → IR: the annotated type by name,
/// `target_info` + `type_path` as raw bytes (JVMS §4.7.20.1-2 carry no
/// pool references, so they survive the rebuild verbatim).
fn type_annotation(
pool: &ConstantPool<'_>,
anno: &ristretto_classfile::attributes::TypeAnnotation,
) -> Result<TypeAnnotation, ClassfileError> {
let mut target = Vec::new();
anno.target_type
.to_bytes(&mut target)
.map_err(|e| ClassfileError::Parse(e.to_string()))?;
let path_len = u8::try_from(anno.type_path.len())
.map_err(|_| ClassfileError::Parse("слишком длинный type_path".into()))?;
target.push(path_len);
for path in &anno.type_path {
path.to_bytes(&mut target)
.map_err(|e| ClassfileError::Parse(e.to_string()))?;
}
Ok(TypeAnnotation {
target,
descriptor: utf8(pool, anno.type_index)?,
elements: anno
.elements
.iter()
.map(|pair| {
Ok((
utf8(pool, pair.name_index)?,
element_value(pool, &pair.value)?,
))
})
.collect::<Result<_, ClassfileError>>()?,
})
}
/// The `CONSTANT_Integer` behind a `B`/`C`/`S`/`Z`/`I` element.
fn const_int(pool: &ConstantPool<'_>, index: u16) -> Result<i32, ClassfileError> {
match const_from(pool, index)? {
Const::Int(v) => Ok(v),
_ => Err(ClassfileError::Parse("не Integer".into())),
}
}
fn const_long(pool: &ConstantPool<'_>, index: u16) -> Result<i64, ClassfileError> {
match const_from(pool, index)? {
Const::Long(v) => Ok(v),
_ => Err(ClassfileError::Parse("не Long".into())),
}
}
fn const_float(pool: &ConstantPool<'_>, index: u16) -> Result<f32, ClassfileError> {
match const_from(pool, index)? {
Const::Float(v) => Ok(v),
_ => Err(ClassfileError::Parse("не Float".into())),
}
}
fn const_double(pool: &ConstantPool<'_>, index: u16) -> Result<f64, ClassfileError> {
match const_from(pool, index)? {
Const::Double(v) => Ok(v),
_ => Err(ClassfileError::Parse("не Double".into())),
}
}

View file

@ -9,8 +9,9 @@
use ristretto_classfile::ConstantPool;
use ristretto_classfile::attributes::Attribute as RtAttr;
use super::annos::{annos, element_value, params, type_annos};
use super::pool::{
annotation, class_name, const_from, is_interface, kind_code, name_and_type, ref_parts, utf8,
class_name, const_from, is_interface, kind_code, name_and_type, ref_parts, utf8,
};
use crate::error::ClassfileError;
use crate::model::{Attribute, BootstrapMethod, InnerClass, MethodParameter, RecordComponent};
@ -108,51 +109,28 @@ pub(super) fn convert(attr: &RtAttr, pool: &ConstantPool<'_>) -> Result<Attribut
));
}
RtAttr::RuntimeVisibleAnnotations { annotations, .. } => {
Attribute::RuntimeVisibleAnnotations(
annotations
.iter()
.map(|anno| annotation(pool, anno))
.collect::<Result<_, _>>()?,
)
Attribute::RuntimeVisibleAnnotations(annos(pool, annotations)?)
}
RtAttr::RuntimeInvisibleAnnotations { annotations, .. } => {
Attribute::RuntimeInvisibleAnnotations(
annotations
.iter()
.map(|anno| annotation(pool, anno))
.collect::<Result<_, _>>()?,
)
Attribute::RuntimeInvisibleAnnotations(annos(pool, annotations)?)
}
RtAttr::RuntimeVisibleParameterAnnotations {
parameter_annotations,
..
} => Attribute::RuntimeVisibleParameterAnnotations(
parameter_annotations
.iter()
.map(|param| {
param
.annotations
.iter()
.map(|anno| annotation(pool, anno))
.collect::<Result<_, _>>()
})
.collect::<Result<_, _>>()?,
),
} => Attribute::RuntimeVisibleParameterAnnotations(params(pool, parameter_annotations)?),
RtAttr::RuntimeInvisibleParameterAnnotations {
parameter_annotations,
..
} => Attribute::RuntimeInvisibleParameterAnnotations(
parameter_annotations
.iter()
.map(|param| {
param
.annotations
.iter()
.map(|anno| annotation(pool, anno))
.collect::<Result<_, _>>()
})
.collect::<Result<_, _>>()?,
),
} => Attribute::RuntimeInvisibleParameterAnnotations(params(pool, parameter_annotations)?),
RtAttr::RuntimeVisibleTypeAnnotations {
type_annotations, ..
} => Attribute::RuntimeVisibleTypeAnnotations(type_annos(pool, type_annotations)?),
RtAttr::RuntimeInvisibleTypeAnnotations {
type_annotations, ..
} => Attribute::RuntimeInvisibleTypeAnnotations(type_annos(pool, type_annotations)?),
RtAttr::AnnotationDefault { element, .. } => {
Attribute::AnnotationDefault(element_value(pool, element)?)
}
other => raw(other)?,
})
}

View file

@ -5,6 +5,7 @@
//! is already split by responsibility so wiring is a single `mod parse` with no
//! refactoring.
pub(super) mod annos;
pub(super) mod attrs;
pub(super) mod class;
pub(super) mod code;

View file

@ -4,7 +4,6 @@ use ristretto_classfile::{Constant, ConstantPool, ReferenceKind};
use crate::error::ClassfileError;
use crate::insn::{Const, InvokeKind};
use crate::model::{Annotation, AnnotationValue};
/// The class name from a `CONSTANT_Class`.
pub(super) fn class_name(pool: &ConstantPool<'_>, index: u16) -> Result<String, ClassfileError> {
@ -183,97 +182,3 @@ pub(super) fn expect_kind(
)))
}
}
/// One `annotation` structure → IR (names, not pool indices).
pub(super) fn annotation(
pool: &ConstantPool<'_>,
anno: &ristretto_classfile::attributes::Annotation,
) -> Result<Annotation, ClassfileError> {
Ok(Annotation {
descriptor: utf8(pool, anno.type_index)?,
elements: anno
.elements
.iter()
.map(|pair| Ok((utf8(pool, pair.name_index)?, element(pool, &pair.value)?)))
.collect::<Result<_, ClassfileError>>()?,
})
}
/// One `element_value` → IR, keeping the tag (HotSpot checks it).
fn element(
pool: &ConstantPool<'_>,
value: &ristretto_classfile::attributes::AnnotationElement,
) -> Result<AnnotationValue, ClassfileError> {
use ristretto_classfile::attributes::AnnotationElement as E;
Ok(match value {
E::Byte { const_value_index } => {
AnnotationValue::Byte(const_int(pool, *const_value_index)?)
}
E::Char { const_value_index } => {
AnnotationValue::Char(const_int(pool, *const_value_index)?)
}
E::Short { const_value_index } => {
AnnotationValue::Short(const_int(pool, *const_value_index)?)
}
E::Boolean { const_value_index } => {
AnnotationValue::Boolean(const_int(pool, *const_value_index)?)
}
E::Int { const_value_index } => AnnotationValue::Int(const_int(pool, *const_value_index)?),
E::Long { const_value_index } => {
AnnotationValue::Long(const_long(pool, *const_value_index)?)
}
E::Float { const_value_index } => {
AnnotationValue::Float(const_float(pool, *const_value_index)?)
}
E::Double { const_value_index } => {
AnnotationValue::Double(const_double(pool, *const_value_index)?)
}
E::String { const_value_index } => AnnotationValue::String(utf8(pool, *const_value_index)?),
E::Enum {
type_name_index,
const_name_index,
} => AnnotationValue::Enum {
descriptor: utf8(pool, *type_name_index)?,
name: utf8(pool, *const_name_index)?,
},
E::Class { class_info_index } => AnnotationValue::Class(utf8(pool, *class_info_index)?),
E::Annotation { annotation: nested } => {
AnnotationValue::Nested(Box::new(annotation(pool, nested)?))
}
E::Array { values } => AnnotationValue::Array(
values
.iter()
.map(|value| element(pool, value))
.collect::<Result<_, _>>()?,
),
})
}
/// The `CONSTANT_Integer` behind a `B`/`C`/`S`/`Z`/`I` element.
fn const_int(pool: &ConstantPool<'_>, index: u16) -> Result<i32, ClassfileError> {
match const_from(pool, index)? {
Const::Int(v) => Ok(v),
_ => Err(ClassfileError::Parse("не Integer".into())),
}
}
fn const_long(pool: &ConstantPool<'_>, index: u16) -> Result<i64, ClassfileError> {
match const_from(pool, index)? {
Const::Long(v) => Ok(v),
_ => Err(ClassfileError::Parse("не Long".into())),
}
}
fn const_float(pool: &ConstantPool<'_>, index: u16) -> Result<f32, ClassfileError> {
match const_from(pool, index)? {
Const::Float(v) => Ok(v),
_ => Err(ClassfileError::Parse("не Float".into())),
}
}
fn const_double(pool: &ConstantPool<'_>, index: u16) -> Result<f64, ClassfileError> {
match const_from(pool, index)? {
Const::Double(v) => Ok(v),
_ => Err(ClassfileError::Parse("не Double".into())),
}
}

View file

@ -2,12 +2,46 @@
use ristretto_classfile::attributes::{
Annotation as RtAnno, AnnotationElement as RtElem, AnnotationValuePair as RtPair,
ParameterAnnotation as RtParamAnno, TargetPath as RtPath, TargetType as RtTarget,
TypeAnnotation as RtTypeAnno,
};
use ristretto_classfile::byte_reader::ByteReader;
use super::pool::Pool;
use crate::error::ClassfileError;
use crate::insn::Const;
use crate::model::{Annotation, AnnotationValue};
use crate::model::{Annotation, AnnotationValue, TypeAnnotation};
/// The `annotations` list of a visible/invisible annotation attribute.
pub(super) fn list(annos: &[Annotation], pool: &mut Pool) -> Result<Vec<RtAnno>, ClassfileError> {
annos.iter().map(|anno| annotation(anno, pool)).collect()
}
/// The per-parameter lists of a parameter-annotation attribute.
pub(super) fn parameters(
params: &[Vec<Annotation>],
pool: &mut Pool,
) -> Result<Vec<RtParamAnno>, ClassfileError> {
params
.iter()
.map(|param| {
Ok(RtParamAnno {
annotations: list(param, pool)?,
})
})
.collect()
}
/// The `type_annotations` list of a type-annotation attribute.
pub(super) fn type_list(
annos: &[TypeAnnotation],
pool: &mut Pool,
) -> Result<Vec<RtTypeAnno>, ClassfileError> {
annos
.iter()
.map(|anno| type_annotation(anno, pool))
.collect()
}
/// One IR annotation → backend structure (fresh pool indices).
pub(super) fn annotation(anno: &Annotation, pool: &mut Pool) -> Result<RtAnno, ClassfileError> {
Ok(RtAnno {
@ -25,8 +59,45 @@ pub(super) fn annotation(anno: &Annotation, pool: &mut Pool) -> Result<RtAnno, C
})
}
/// One IR type annotation → backend structure (fresh pool indices). The
/// opaque `target` bytes (`target_info` + `type_path`) carry no pool
/// references and are re-deserialized into backend structs verbatim.
pub(super) fn type_annotation(
anno: &TypeAnnotation,
pool: &mut Pool,
) -> Result<RtTypeAnno, ClassfileError> {
let mut reader = ByteReader::new(&anno.target);
let target_type = RtTarget::from_bytes(&mut reader)
.map_err(|e| ClassfileError::Write(format!("битый target_info: {e}")))?;
let path_len = reader
.read_u8()
.map_err(|e| ClassfileError::Write(format!("битый type_path: {e}")))?;
let mut type_path = Vec::with_capacity(path_len as usize);
for _ in 0..path_len {
type_path.push(
RtPath::from_bytes(&mut reader)
.map_err(|e| ClassfileError::Write(format!("битый type_path: {e}")))?,
);
}
Ok(RtTypeAnno {
target_type,
type_path,
type_index: pool.utf8(&anno.descriptor)?,
elements: anno
.elements
.iter()
.map(|(name, value)| {
Ok(RtPair {
name_index: pool.utf8(name)?,
value: element(value, pool)?,
})
})
.collect::<Result<_, ClassfileError>>()?,
})
}
/// One IR element value → backend structure, tag preserved.
fn element(value: &AnnotationValue, pool: &mut Pool) -> Result<RtElem, ClassfileError> {
pub(super) fn element(value: &AnnotationValue, pool: &mut Pool) -> Result<RtElem, ClassfileError> {
fn int(v: i32, pool: &mut Pool) -> Result<u16, ClassfileError> {
pool.constant(&Const::Int(v))
}

View file

@ -8,7 +8,7 @@ use ristretto_classfile::attributes::{
LocalVariableTypeTable as RtLocalType, Record as RtRecord,
};
use super::annotations::annotation;
use super::annotations;
use super::helpers;
use super::pool::Pool;
use crate::error::ClassfileError;
@ -181,50 +181,38 @@ pub(super) fn convert(attr: &Attribute, pool: &mut Pool) -> Result<RtAttr, Class
},
Attribute::RuntimeVisibleAnnotations(annos) => RtAttr::RuntimeVisibleAnnotations {
name_index: pool.utf8("RuntimeVisibleAnnotations")?,
annotations: annos
.iter()
.map(|anno| annotation(anno, pool))
.collect::<Result<_, _>>()?,
annotations: annotations::list(annos, pool)?,
},
Attribute::RuntimeInvisibleAnnotations(annos) => RtAttr::RuntimeInvisibleAnnotations {
name_index: pool.utf8("RuntimeInvisibleAnnotations")?,
annotations: annos
.iter()
.map(|anno| annotation(anno, pool))
.collect::<Result<_, _>>()?,
annotations: annotations::list(annos, pool)?,
},
Attribute::RuntimeVisibleParameterAnnotations(params) => {
RtAttr::RuntimeVisibleParameterAnnotations {
name_index: pool.utf8("RuntimeVisibleParameterAnnotations")?,
parameter_annotations: params
.iter()
.map(|param| {
Ok(ristretto_classfile::attributes::ParameterAnnotation {
annotations: param
.iter()
.map(|anno| annotation(anno, pool))
.collect::<Result<_, _>>()?,
})
})
.collect::<Result<_, _>>()?,
parameter_annotations: annotations::parameters(params, pool)?,
}
}
Attribute::RuntimeInvisibleParameterAnnotations(params) => {
RtAttr::RuntimeInvisibleParameterAnnotations {
name_index: pool.utf8("RuntimeInvisibleParameterAnnotations")?,
parameter_annotations: params
.iter()
.map(|param| {
Ok(ristretto_classfile::attributes::ParameterAnnotation {
annotations: param
.iter()
.map(|anno| annotation(anno, pool))
.collect::<Result<_, _>>()?,
})
})
.collect::<Result<_, _>>()?,
parameter_annotations: annotations::parameters(params, pool)?,
}
}
Attribute::RuntimeVisibleTypeAnnotations(annos) => RtAttr::RuntimeVisibleTypeAnnotations {
name_index: pool.utf8("RuntimeVisibleTypeAnnotations")?,
type_annotations: annotations::type_list(annos, pool)?,
},
Attribute::RuntimeInvisibleTypeAnnotations(annos) => {
RtAttr::RuntimeInvisibleTypeAnnotations {
name_index: pool.utf8("RuntimeInvisibleTypeAnnotations")?,
type_annotations: annotations::type_list(annos, pool)?,
}
}
Attribute::AnnotationDefault(value) => RtAttr::AnnotationDefault {
name_index: pool.utf8("AnnotationDefault")?,
element: annotations::element(value, pool)?,
},
Attribute::Raw { name, data } => RtAttr::Unknown {
name_index: pool.utf8(name)?,
info: data.clone(),

View file

@ -25,7 +25,7 @@ pub use model::builder::{ClassBuilder, empty_code};
pub use model::{
Annotation, AnnotationValue, Attribute, BootstrapMethod, ClassFile, Code, ExceptionHandler,
Field, InnerClass, LineNumber, LocalVariable, LocalVariableType, Method, MethodParameter,
Program, RecordComponent, Resource, Version,
Program, RecordComponent, Resource, TypeAnnotation, Version,
};
/// Safety budget: a method body that would grow past this many instructions

View file

@ -13,6 +13,19 @@ pub struct Annotation {
pub elements: Vec<(String, AnnotationValue)>,
}
/// One `type_annotation` (JVMS §4.7.20): a type-use annotation on a
/// method return type, a cast, a local variable, and so on.
#[derive(Debug, Clone, PartialEq)]
pub struct TypeAnnotation {
/// `target_info` + `type_path` bytes (§4.7.20.1-2): no pool references
/// there at all, kept opaque so write copies them verbatim.
pub target: Vec<u8>,
/// The annotated type as a field descriptor (`type_index`).
pub descriptor: String,
/// `(element name, value)` pairs - same shape as [`Annotation`]'s.
pub elements: Vec<(String, AnnotationValue)>,
}
/// A single `element_value`: one variant per tag, so `B`/`C`/`S`/`Z`
/// keep their identity (the pool stores them all as `CONSTANT_Integer`,
/// but HotSpot checks the tag against the annotation member's type).

View file

@ -4,7 +4,7 @@ pub mod annotations;
pub mod builder;
pub mod version;
pub use annotations::{Annotation, AnnotationValue};
pub use annotations::{Annotation, AnnotationValue, TypeAnnotation};
pub use version::Version;
use crate::format::StackMapFrame;
@ -54,6 +54,15 @@ pub enum Attribute {
RuntimeVisibleParameterAnnotations(Vec<Vec<Annotation>>),
/// `RuntimeInvisibleParameterAnnotations` (JVMS §4.7.19).
RuntimeInvisibleParameterAnnotations(Vec<Vec<Annotation>>),
/// `RuntimeVisibleTypeAnnotations` (JVMS §4.7.20): type-use
/// annotations. Parsed - raw pool indices go stale on write and ASM
/// dies with `ArrayIndexOutOfBoundsException` reading them back.
RuntimeVisibleTypeAnnotations(Vec<TypeAnnotation>),
/// `RuntimeInvisibleTypeAnnotations` (JVMS §4.7.21).
RuntimeInvisibleTypeAnnotations(Vec<TypeAnnotation>),
/// `AnnotationDefault` (JVMS §4.7.22): the default value of one
/// annotation member - a single `element_value` with pool references.
AnnotationDefault(AnnotationValue),
/// Everything else: name + raw `info` bytes (careful with pool indices).
Raw {
name: String,

View file

@ -170,3 +170,4 @@ fn round_trip(rt: &RtClass<'_>) -> ClassFile {
/// instead of dying in the backend's strict verifier on write.
mod attrs;
mod code;
mod type_annotations;

View file

@ -0,0 +1,259 @@
//! Type annotations and `AnnotationDefault` must be modeled, not passed
//! through as raw bytes: the writer rebuilds the pool, so stale indices
//! point at the wrong utf8 or past the pool end. ASM dies on those with
//! `ArrayIndexOutOfBoundsException` (Paper's plugin converter on real jars).
#![allow(clippy::expect_used, clippy::unwrap_used)]
use noxis_classfile::format;
use ristretto_classfile::attributes::{
AnnotationElement as RtElem, AnnotationValuePair as RtPair, Attribute as RtAttr,
Instruction as Rt, TargetType as RtTarget, TypeAnnotation as RtTypeAnno,
};
use ristretto_classfile::{
ClassAccessFlags, ClassFile as RtClass, ConstantPool, JAVA_21, Method as RtMethod,
MethodAccessFlags,
};
/// Method-level `RuntimeVisibleTypeAnnotations` (target `0x14`, method
/// return type): after the write rebuilds the pool, `type_index` and the
/// element references must resolve to the same strings as before.
#[test]
fn method_type_annotation_index_is_rebuilt() {
let mut pool = ConstantPool::default();
let this = pool.add_class("demo/TA").expect("this");
let object = pool.add_class("java/lang/Object").expect("object");
let name = pool.add_utf8("run").expect("name");
let desc = pool.add_utf8("()Ljava/lang/Object;").expect("desc");
let attr = pool
.add_utf8("RuntimeVisibleTypeAnnotations")
.expect("attr");
let type_desc = pool.add_utf8("Ldemo/Use;").expect("type");
let value_name = pool.add_utf8("value").expect("value");
let enum_desc = pool.add_utf8("Ldemo/Kind;").expect("enum desc");
let enum_const = pool.add_utf8("A").expect("enum const");
let rt = RtClass {
version: JAVA_21,
constant_pool: pool,
access_flags: ClassAccessFlags::PUBLIC | ClassAccessFlags::SUPER,
this_class: this,
super_class: object,
interfaces: vec![],
fields: vec![],
methods: vec![RtMethod {
access_flags: MethodAccessFlags::PUBLIC | MethodAccessFlags::ABSTRACT,
name_index: name,
descriptor_index: desc,
attributes: vec![RtAttr::RuntimeVisibleTypeAnnotations {
name_index: attr,
type_annotations: vec![RtTypeAnno {
target_type: RtTarget::Empty { target_type: 0x14 },
type_path: vec![],
type_index: type_desc,
elements: vec![RtPair {
name_index: value_name,
value: RtElem::Enum {
type_name_index: enum_desc,
const_name_index: enum_const,
},
}],
}],
}],
}],
attributes: vec![],
..RtClass::default()
};
let written = rewritten(&rt);
let back = RtClass::from_bytes(&written).expect("reparse");
let anno = back.methods[0]
.attributes
.iter()
.find_map(|a| match a {
RtAttr::RuntimeVisibleTypeAnnotations {
type_annotations, ..
} => type_annotations.first(),
_ => None,
})
.expect("type annotation modeled, not Raw");
let resolved = back
.constant_pool
.try_get_utf8(anno.type_index)
.expect("type_index valid")
.to_string();
assert_eq!(resolved, "Ldemo/Use;");
let pair = anno.elements.first().expect("one element pair");
let pair_name = back
.constant_pool
.try_get_utf8(pair.name_index)
.expect("element name valid")
.to_string();
assert_eq!(pair_name, "value");
let RtElem::Enum {
type_name_index,
const_name_index,
} = &pair.value
else {
panic!("enum element stays an enum");
};
assert_eq!(
back.constant_pool
.try_get_utf8(*type_name_index)
.expect("enum type valid")
.to_string(),
"Ldemo/Kind;"
);
assert_eq!(
back.constant_pool
.try_get_utf8(*const_name_index)
.expect("enum const valid")
.to_string(),
"A"
);
}
/// Code-level `RuntimeInvisibleTypeAnnotations` (cast type argument,
/// target `0x48`): the attribute lives inside `Code`, same rebuild rule.
#[test]
fn code_type_annotation_index_is_rebuilt() {
let mut pool = ConstantPool::default();
let this = pool.add_class("demo/CTA").expect("this");
let object = pool.add_class("java/lang/Object").expect("object");
let name = pool.add_utf8("run").expect("name");
let desc = pool.add_utf8("()V").expect("desc");
let code_name = pool.add_utf8("Code").expect("code");
let attr = pool
.add_utf8("RuntimeInvisibleTypeAnnotations")
.expect("attr");
let type_desc = pool.add_utf8("Llocal/Use;").expect("type");
let rt = RtClass {
version: JAVA_21,
constant_pool: pool,
access_flags: ClassAccessFlags::PUBLIC | ClassAccessFlags::SUPER,
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![RtAttr::Code {
name_index: code_name,
max_stack: 1,
max_locals: 0,
code: vec![Rt::Aconst_null, Rt::Pop, Rt::Return],
exception_table: vec![],
attributes: vec![RtAttr::RuntimeInvisibleTypeAnnotations {
name_index: attr,
type_annotations: vec![RtTypeAnno {
target_type: RtTarget::TypeArgument {
target_type: 0x48,
offset: 0,
type_argument_index: 0,
},
type_path: vec![],
type_index: type_desc,
elements: vec![],
}],
}],
}],
}],
attributes: vec![],
..RtClass::default()
};
let written = rewritten(&rt);
let back = RtClass::from_bytes(&written).expect("reparse");
let code = back.methods[0]
.attributes
.iter()
.find_map(|a| match a {
RtAttr::Code { attributes, .. } => Some(attributes),
_ => None,
})
.expect("code");
let anno = code
.iter()
.find_map(|a| match a {
RtAttr::RuntimeInvisibleTypeAnnotations {
type_annotations, ..
} => type_annotations.first(),
_ => None,
})
.expect("type annotation modeled, not Raw");
let resolved = back
.constant_pool
.try_get_utf8(anno.type_index)
.expect("type_index valid")
.to_string();
assert_eq!(resolved, "Llocal/Use;");
}
/// `AnnotationDefault` holds one `element_value` with pool references -
/// same stale-index hazard as the annotations themselves.
#[test]
fn annotation_default_index_is_rebuilt() {
let mut pool = ConstantPool::default();
let this = pool.add_class("demo/Def").expect("this");
let object = pool.add_class("java/lang/Object").expect("object");
let name = pool.add_utf8("value").expect("name");
let desc = pool.add_utf8("()Ljava/lang/String;").expect("desc");
let attr = pool.add_utf8("AnnotationDefault").expect("attr");
let value = pool.add_utf8("x").expect("value");
let rt = RtClass {
version: JAVA_21,
constant_pool: pool,
access_flags: ClassAccessFlags::PUBLIC
| ClassAccessFlags::INTERFACE
| ClassAccessFlags::ABSTRACT,
this_class: this,
super_class: object,
interfaces: vec![],
fields: vec![],
methods: vec![RtMethod {
access_flags: MethodAccessFlags::PUBLIC | MethodAccessFlags::ABSTRACT,
name_index: name,
descriptor_index: desc,
attributes: vec![RtAttr::AnnotationDefault {
name_index: attr,
element: RtElem::String {
const_value_index: value,
},
}],
}],
attributes: vec![],
..RtClass::default()
};
let written = rewritten(&rt);
let back = RtClass::from_bytes(&written).expect("reparse");
let element = back.methods[0]
.attributes
.iter()
.find_map(|a| match a {
RtAttr::AnnotationDefault { element, .. } => Some(element),
_ => None,
})
.expect("AnnotationDefault modeled, not Raw");
let RtElem::String { const_value_index } = element else {
panic!("default stays a string element");
};
let resolved = back
.constant_pool
.try_get_utf8(*const_value_index)
.expect("const_value_index valid")
.to_string();
assert_eq!(resolved, "x");
}
/// `parse → write` via our IR; the rewritten bytes for re-parsing.
fn rewritten(rt: &RtClass<'_>) -> Vec<u8> {
let mut original = Vec::new();
rt.to_bytes(&mut original).expect("to_bytes");
let ir = format::parse(&original).expect("parse");
format::write(&ir).expect("write")
}