diff --git a/crates/noxis-classfile/src/format/parse/annos/mod.rs b/crates/noxis-classfile/src/format/parse/annos/mod.rs new file mode 100644 index 0000000..a365c07 --- /dev/null +++ b/crates/noxis-classfile/src/format/parse/annos/mod.rs @@ -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, 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>, 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, 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 { + 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::>()?, + ), + }) +} + +/// One `annotation` structure → IR (names, not pool indices). +fn annotation( + pool: &ConstantPool<'_>, + anno: &ristretto_classfile::attributes::Annotation, +) -> Result { + 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::>()?, + }) +} + +/// 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 { + 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::>()?, + }) +} + +/// The `CONSTANT_Integer` behind a `B`/`C`/`S`/`Z`/`I` element. +fn const_int(pool: &ConstantPool<'_>, index: u16) -> Result { + match const_from(pool, index)? { + Const::Int(v) => Ok(v), + _ => Err(ClassfileError::Parse("не Integer".into())), + } +} + +fn const_long(pool: &ConstantPool<'_>, index: u16) -> Result { + match const_from(pool, index)? { + Const::Long(v) => Ok(v), + _ => Err(ClassfileError::Parse("не Long".into())), + } +} + +fn const_float(pool: &ConstantPool<'_>, index: u16) -> Result { + match const_from(pool, index)? { + Const::Float(v) => Ok(v), + _ => Err(ClassfileError::Parse("не Float".into())), + } +} + +fn const_double(pool: &ConstantPool<'_>, index: u16) -> Result { + match const_from(pool, index)? { + Const::Double(v) => Ok(v), + _ => Err(ClassfileError::Parse("не Double".into())), + } +} diff --git a/crates/noxis-classfile/src/format/parse/attrs.rs b/crates/noxis-classfile/src/format/parse/attrs.rs index 66cad3d..c3eb382 100644 --- a/crates/noxis-classfile/src/format/parse/attrs.rs +++ b/crates/noxis-classfile/src/format/parse/attrs.rs @@ -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 { - Attribute::RuntimeVisibleAnnotations( - annotations - .iter() - .map(|anno| annotation(pool, anno)) - .collect::>()?, - ) + Attribute::RuntimeVisibleAnnotations(annos(pool, annotations)?) } RtAttr::RuntimeInvisibleAnnotations { annotations, .. } => { - Attribute::RuntimeInvisibleAnnotations( - annotations - .iter() - .map(|anno| annotation(pool, anno)) - .collect::>()?, - ) + 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::>() - }) - .collect::>()?, - ), + } => 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::>() - }) - .collect::>()?, - ), + } => 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)?, }) } diff --git a/crates/noxis-classfile/src/format/parse/mod.rs b/crates/noxis-classfile/src/format/parse/mod.rs index 940481b..38927ad 100644 --- a/crates/noxis-classfile/src/format/parse/mod.rs +++ b/crates/noxis-classfile/src/format/parse/mod.rs @@ -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; diff --git a/crates/noxis-classfile/src/format/parse/pool.rs b/crates/noxis-classfile/src/format/parse/pool.rs index bda169c..8908611 100644 --- a/crates/noxis-classfile/src/format/parse/pool.rs +++ b/crates/noxis-classfile/src/format/parse/pool.rs @@ -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 { @@ -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 { - 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::>()?, - }) -} - -/// One `element_value` → IR, keeping the tag (HotSpot checks it). -fn element( - pool: &ConstantPool<'_>, - value: &ristretto_classfile::attributes::AnnotationElement, -) -> Result { - 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::>()?, - ), - }) -} - -/// The `CONSTANT_Integer` behind a `B`/`C`/`S`/`Z`/`I` element. -fn const_int(pool: &ConstantPool<'_>, index: u16) -> Result { - match const_from(pool, index)? { - Const::Int(v) => Ok(v), - _ => Err(ClassfileError::Parse("не Integer".into())), - } -} - -fn const_long(pool: &ConstantPool<'_>, index: u16) -> Result { - match const_from(pool, index)? { - Const::Long(v) => Ok(v), - _ => Err(ClassfileError::Parse("не Long".into())), - } -} - -fn const_float(pool: &ConstantPool<'_>, index: u16) -> Result { - match const_from(pool, index)? { - Const::Float(v) => Ok(v), - _ => Err(ClassfileError::Parse("не Float".into())), - } -} - -fn const_double(pool: &ConstantPool<'_>, index: u16) -> Result { - match const_from(pool, index)? { - Const::Double(v) => Ok(v), - _ => Err(ClassfileError::Parse("не Double".into())), - } -} diff --git a/crates/noxis-classfile/src/format/write/annotations.rs b/crates/noxis-classfile/src/format/write/annotations.rs index 4fab58c..e62fc39 100644 --- a/crates/noxis-classfile/src/format/write/annotations.rs +++ b/crates/noxis-classfile/src/format/write/annotations.rs @@ -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, ClassfileError> { + annos.iter().map(|anno| annotation(anno, pool)).collect() +} + +/// The per-parameter lists of a parameter-annotation attribute. +pub(super) fn parameters( + params: &[Vec], + pool: &mut Pool, +) -> Result, 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, 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 { Ok(RtAnno { @@ -25,8 +59,45 @@ pub(super) fn annotation(anno: &Annotation, pool: &mut Pool) -> Result Result { + 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::>()?, + }) +} + /// One IR element value → backend structure, tag preserved. -fn element(value: &AnnotationValue, pool: &mut Pool) -> Result { +pub(super) fn element(value: &AnnotationValue, pool: &mut Pool) -> Result { fn int(v: i32, pool: &mut Pool) -> Result { pool.constant(&Const::Int(v)) } diff --git a/crates/noxis-classfile/src/format/write/attrs.rs b/crates/noxis-classfile/src/format/write/attrs.rs index a450715..8418b47 100644 --- a/crates/noxis-classfile/src/format/write/attrs.rs +++ b/crates/noxis-classfile/src/format/write/attrs.rs @@ -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::RuntimeVisibleAnnotations { name_index: pool.utf8("RuntimeVisibleAnnotations")?, - annotations: annos - .iter() - .map(|anno| annotation(anno, pool)) - .collect::>()?, + annotations: annotations::list(annos, pool)?, }, Attribute::RuntimeInvisibleAnnotations(annos) => RtAttr::RuntimeInvisibleAnnotations { name_index: pool.utf8("RuntimeInvisibleAnnotations")?, - annotations: annos - .iter() - .map(|anno| annotation(anno, pool)) - .collect::>()?, + 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::>()?, - }) - }) - .collect::>()?, + 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::>()?, - }) - }) - .collect::>()?, + 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(), diff --git a/crates/noxis-classfile/src/lib.rs b/crates/noxis-classfile/src/lib.rs index a69879e..cb5e1df 100644 --- a/crates/noxis-classfile/src/lib.rs +++ b/crates/noxis-classfile/src/lib.rs @@ -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 diff --git a/crates/noxis-classfile/src/model/annotations.rs b/crates/noxis-classfile/src/model/annotations.rs index 0a5efc2..17b88e1 100644 --- a/crates/noxis-classfile/src/model/annotations.rs +++ b/crates/noxis-classfile/src/model/annotations.rs @@ -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, + /// 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). diff --git a/crates/noxis-classfile/src/model/mod.rs b/crates/noxis-classfile/src/model/mod.rs index ecedd92..64302f3 100644 --- a/crates/noxis-classfile/src/model/mod.rs +++ b/crates/noxis-classfile/src/model/mod.rs @@ -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>), /// `RuntimeInvisibleParameterAnnotations` (JVMS §4.7.19). RuntimeInvisibleParameterAnnotations(Vec>), + /// `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), + /// `RuntimeInvisibleTypeAnnotations` (JVMS §4.7.21). + RuntimeInvisibleTypeAnnotations(Vec), + /// `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, diff --git a/crates/noxis-classfile/tests/roundtrip/main.rs b/crates/noxis-classfile/tests/roundtrip/main.rs index 4e09373..fc3279d 100644 --- a/crates/noxis-classfile/tests/roundtrip/main.rs +++ b/crates/noxis-classfile/tests/roundtrip/main.rs @@ -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; diff --git a/crates/noxis-classfile/tests/roundtrip/type_annotations.rs b/crates/noxis-classfile/tests/roundtrip/type_annotations.rs new file mode 100644 index 0000000..e80a780 --- /dev/null +++ b/crates/noxis-classfile/tests/roundtrip/type_annotations.rs @@ -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 { + 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") +} diff --git a/crates/noxis-passes/src/rename/rewrite/annotations.rs b/crates/noxis-passes/src/rename/rewrite/annotations.rs index 4ee45df..a585ac0 100644 --- a/crates/noxis-passes/src/rename/rewrite/annotations.rs +++ b/crates/noxis-passes/src/rename/rewrite/annotations.rs @@ -1,7 +1,7 @@ //! Annotation rewriting: class descriptors follow the mapping, string //! values are data and stay put. -use noxis_classfile::{Annotation, AnnotationValue}; +use noxis_classfile::{Annotation, AnnotationValue, TypeAnnotation}; use super::super::assign::{RenameMaps, rewrite_descriptor}; @@ -19,6 +19,18 @@ pub(super) fn rewrite_parameter_annotations(params: &mut [Vec], maps } } +/// Type-use annotations (`RuntimeVisibleTypeAnnotations` and friends): +/// the annotated type descriptor follows the mapping, the opaque +/// `target_info`/`type_path` bytes carry no pool references at all. +pub(super) fn rewrite_type_annotations(annos: &mut [TypeAnnotation], maps: &RenameMaps) { + for anno in annos.iter_mut() { + anno.descriptor = rewrite_descriptor(&anno.descriptor, &maps.classes); + for (_, value) in anno.elements.iter_mut() { + rewrite_value(value, maps); + } + } +} + fn rewrite_annotation(anno: &mut Annotation, maps: &RenameMaps) { anno.descriptor = rewrite_descriptor(&anno.descriptor, &maps.classes); for (_, value) in anno.elements.iter_mut() { @@ -26,7 +38,7 @@ fn rewrite_annotation(anno: &mut Annotation, maps: &RenameMaps) { } } -fn rewrite_value(value: &mut AnnotationValue, maps: &RenameMaps) { +pub(super) fn rewrite_value(value: &mut AnnotationValue, maps: &RenameMaps) { match value { // The `'e'` tag: enum type descriptor moves, the constant name does not. AnnotationValue::Enum { descriptor, .. } => { diff --git a/crates/noxis-passes/src/rename/rewrite/consts.rs b/crates/noxis-passes/src/rename/rewrite/consts.rs new file mode 100644 index 0000000..0cc74c9 --- /dev/null +++ b/crates/noxis-passes/src/rename/rewrite/consts.rs @@ -0,0 +1,58 @@ +//! Constant-pool references: owners and descriptors follow the mapping. + +use noxis_classfile::Const; + +use super::super::assign::{RenameMaps, rewrite_descriptor}; + +/// A method reference (invoke/bootstrap/method-handle): the owner follows +/// class renames, the name follows member renames, the descriptor is +/// rewritten. Array owners (`[Lpkg/C;` - e.g. `clone` on arrays) name the +/// element class inside a descriptor, so they take the descriptor path. +pub(super) fn rewrite_method_ref( + owner: &mut String, + name: &mut String, + descriptor: &mut String, + maps: &RenameMaps, +) { + if owner.starts_with('[') { + *owner = rewrite_descriptor(owner, &maps.classes); + } else { + if let Some(new_name) = maps.method(owner, name, descriptor) { + *name = new_name; + } + if let Some(mapped) = maps.classes.get(owner.as_str()) { + *owner = mapped.clone(); + } + } + *descriptor = rewrite_descriptor(descriptor, &maps.classes); +} + +/// A constant-pool reference inside code or bootstrap arguments: class +/// literals (plain or array), method types, method handles, condy payloads. +/// Anything else (numbers, strings) has no references to follow. +pub(super) fn rewrite_const(value: &mut Const, maps: &RenameMaps) { + match value { + Const::Class(name) => { + if name.starts_with('[') { + *name = rewrite_descriptor(name, &maps.classes); + } else if let Some(mapped) = maps.classes.get(name.as_str()) { + *name = mapped.clone(); + } + } + Const::MethodType(descriptor) => { + *descriptor = rewrite_descriptor(descriptor, &maps.classes); + } + Const::MethodHandle { + owner, + name, + descriptor, + .. + } => { + rewrite_method_ref(owner, name, descriptor, maps); + } + Const::Dynamic { descriptor, .. } => { + *descriptor = rewrite_descriptor(descriptor, &maps.classes); + } + _ => {} + } +} diff --git a/crates/noxis-passes/src/rename/rewrite/mod.rs b/crates/noxis-passes/src/rename/rewrite/mod.rs index d6a95ee..b4e1fab 100644 --- a/crates/noxis-passes/src/rename/rewrite/mod.rs +++ b/crates/noxis-passes/src/rename/rewrite/mod.rs @@ -2,9 +2,13 @@ mod annotations; mod code; +mod consts; -use annotations::{rewrite_annotations, rewrite_parameter_annotations}; -use noxis_classfile::{Attribute, ClassFile, Const, StackMapFrame, VerificationType}; +use annotations::{ + rewrite_annotations, rewrite_parameter_annotations, rewrite_type_annotations, rewrite_value, +}; +use consts::{rewrite_const, rewrite_method_ref}; +use noxis_classfile::{Attribute, ClassFile, StackMapFrame, VerificationType}; use noxis_core::{Config, Context}; use super::assign::{RenameMaps, rewrite_descriptor}; @@ -151,6 +155,11 @@ fn rewrite_attributes( | Attribute::RuntimeInvisibleParameterAnnotations(params) => { rewrite_parameter_annotations(params, maps); } + Attribute::RuntimeVisibleTypeAnnotations(annos) + | Attribute::RuntimeInvisibleTypeAnnotations(annos) => { + rewrite_type_annotations(annos, maps); + } + Attribute::AnnotationDefault(value) => rewrite_value(value, maps), Attribute::BootstrapMethods(methods) => { for bsm in methods.iter_mut() { rewrite_method_ref(&mut bsm.owner, &mut bsm.name, &mut bsm.descriptor, maps); @@ -190,59 +199,6 @@ fn rewrite_attributes( } } -/// A method reference (invoke/bootstrap/method-handle): the owner follows -/// class renames, the name follows member renames, the descriptor is -/// rewritten. Array owners (`[Lpkg/C;` - e.g. `clone` on arrays) name the -/// element class inside a descriptor, so they take the descriptor path. -fn rewrite_method_ref( - owner: &mut String, - name: &mut String, - descriptor: &mut String, - maps: &RenameMaps, -) { - if owner.starts_with('[') { - *owner = rewrite_descriptor(owner, &maps.classes); - } else { - if let Some(new_name) = maps.method(owner, name, descriptor) { - *name = new_name; - } - if let Some(mapped) = maps.classes.get(owner.as_str()) { - *owner = mapped.clone(); - } - } - *descriptor = rewrite_descriptor(descriptor, &maps.classes); -} - -/// A constant-pool reference inside code or bootstrap arguments: class -/// literals (plain or array), method types, method handles, condy payloads. -/// Anything else (numbers, strings) has no references to follow. -fn rewrite_const(value: &mut Const, maps: &RenameMaps) { - match value { - Const::Class(name) => { - if name.starts_with('[') { - *name = rewrite_descriptor(name, &maps.classes); - } else if let Some(mapped) = maps.classes.get(name.as_str()) { - *name = mapped.clone(); - } - } - Const::MethodType(descriptor) => { - *descriptor = rewrite_descriptor(descriptor, &maps.classes); - } - Const::MethodHandle { - owner, - name, - descriptor, - .. - } => { - rewrite_method_ref(owner, name, descriptor, maps); - } - Const::Dynamic { descriptor, .. } => { - *descriptor = rewrite_descriptor(descriptor, &maps.classes); - } - _ => {} - } -} - /// Имена классов внутри одного кадра (`Object(..)` в locals/stack). fn rewrite_frame(frame: &mut StackMapFrame, maps: &RenameMaps) { match frame {