From 5849578b17757b3f5b2c7bd62e6b106c6c33c2a0 Mon Sep 17 00:00:00 2001 From: brianheineman Date: Wed, 10 Jul 2024 20:05:40 -0600 Subject: [PATCH] feat: improve Display output to include constant pool, code and line table --- ristretto_classfile/benches/classfile.rs | 10 +++ .../src/attributes/attribute.rs | 80 ++++++++++++++---- .../src/attributes/method_parameter.rs | 13 +-- ristretto_classfile/src/attributes/record.rs | 6 +- ristretto_classfile/src/class_file.rs | 82 ++++++++++++++----- ristretto_classfile/src/constant_pool.rs | 37 ++++++--- ristretto_classfile/src/field.rs | 7 +- ristretto_classfile/src/method.rs | 25 +++--- 8 files changed, 188 insertions(+), 72 deletions(-) diff --git a/ristretto_classfile/benches/classfile.rs b/ristretto_classfile/benches/classfile.rs index 1b8646c2..40586825 100644 --- a/ristretto_classfile/benches/classfile.rs +++ b/ristretto_classfile/benches/classfile.rs @@ -25,6 +25,11 @@ fn bench_lifecycle(criterion: &mut Criterion) -> Result<()> { verify(&class_file).ok(); }); }); + criterion.bench_function("to_string", |bencher| { + bencher.iter(|| { + to_string(&class_file).ok(); + }); + }); Ok(()) } @@ -45,6 +50,11 @@ fn verify(class_file: &ClassFile) -> Result<()> { Ok(()) } +fn to_string(class_file: &ClassFile) -> Result<()> { + class_file.to_string(); + Ok(()) +} + criterion_group!( name = benches; config = Criterion::default(); diff --git a/ristretto_classfile/src/attributes/attribute.rs b/ristretto_classfile/src/attributes/attribute.rs index 8c59740d..1bda6895 100644 --- a/ristretto_classfile/src/attributes/attribute.rs +++ b/ristretto_classfile/src/attributes/attribute.rs @@ -9,6 +9,7 @@ use crate::attributes::{ }; use crate::constant::Constant; use crate::constant_pool::ConstantPool; +use crate::display::indent_lines; use crate::error::Error::{InvalidAttributeLength, InvalidAttributeNameIndex}; use crate::error::Result; use crate::mutf8; @@ -35,7 +36,7 @@ const VERSION_61_0: Version = Version::Java17 { minor: 0 }; pub enum Attribute { ConstantValue { name_index: u16, - constantvalue_index: u16, + constant_value_index: u16, }, Code { name_index: u16, @@ -275,7 +276,7 @@ impl Attribute { } Attribute::ConstantValue { name_index, - constantvalue_index: bytes.read_u16::()?, + constant_value_index: bytes.read_u16::()?, } } "Code" => { @@ -663,8 +664,8 @@ impl Attribute { let (name_index, info) = match self { Attribute::ConstantValue { name_index, - constantvalue_index, - } => (name_index, constantvalue_index.to_be_bytes().to_vec()), + constant_value_index, + } => (name_index, constant_value_index.to_be_bytes().to_vec()), Attribute::Code { name_index, max_stack, @@ -1009,7 +1010,49 @@ impl Attribute { impl fmt::Display for Attribute { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - write!(f, "{self:?}") + match self { + Attribute::Code { + max_stack, + max_locals, + code, + exceptions, + attributes, + .. + } => { + writeln!(f, "Code:")?; + writeln!(f, " max_stack = {max_stack}, max_locals = {max_locals}")?; + + let code_length = u64::try_from(code.len()).map_err(|_| fmt::Error)?; + let mut cursor = Cursor::new(code.clone()); + while cursor.position() < code_length { + let index = cursor.position(); + let instruction = + Instruction::from_bytes(&mut cursor).map_err(|_| fmt::Error)?; + let value = instruction.to_string(); + let (name, value) = value.split_once(' ').unwrap_or((value.as_str(), "")); + writeln!(f, "{index:>6}: {name:<12} {value}")?; + } + + if !exceptions.is_empty() { + writeln!(f, " {exceptions:?}")?; + } + + for attribute in attributes { + writeln!(f, "{}", indent_lines(&attribute.to_string(), " "))?; + } + } + Attribute::LineNumberTable { line_numbers, .. } => { + writeln!(f, "LineNumberTable:")?; + for line_number in line_numbers { + let start_pc = line_number.start_pc; + let line_number = line_number.line_number; + writeln!(f, "{:>9}: {line_number}", format!("line {start_pc}"))?; + } + } + _ => write!(f, "{self:?}")?, + } + + Ok(()) } } @@ -1022,6 +1065,7 @@ mod test { AnnotationElement, ExportsFlags, OpensFlags, RequiresFlags, TargetPath, TargetType, }; use crate::method_access_flags::MethodAccessFlags; + use indoc::indoc; #[test] fn test_invalid_attribute_name_index_error() { @@ -1079,7 +1123,7 @@ mod test { fn test_constant_value() -> Result<()> { let attribute = Attribute::ConstantValue { name_index: 1, - constantvalue_index: 42, + constant_value_index: 42, }; let expected_bytes = [0, 1, 0, 0, 0, 2, 0, 42]; @@ -1090,7 +1134,7 @@ mod test { fn test_code() -> Result<()> { let constant = Attribute::ConstantValue { name_index: 2, - constantvalue_index: 42, + constant_value_index: 42, }; let exception = CodeException { start_pc: 1, @@ -1110,8 +1154,15 @@ mod test { 0, 1, 0, 0, 0, 29, 0, 2, 0, 3, 0, 0, 0, 1, 4, 0, 1, 0, 1, 0, 2, 0, 3, 0, 4, 0, 1, 0, 2, 0, 0, 0, 2, 0, 42, ]; + let expected = indoc! {" + Code: + max_stack = 2, max_locals = 3 + 0: iconst_1 + [CodeException { start_pc: 1, end_pc: 2, handler_pc: 3, catch_type: 4 }] + ConstantValue { name_index: 2, constant_value_index: 42 } + "}; - assert_eq!("Code { name_index: 1, max_stack: 2, max_locals: 3, code: [4], exceptions: [CodeException { start_pc: 1, end_pc: 2, handler_pc: 3, catch_type: 4 }], attributes: [ConstantValue { name_index: 2, constantvalue_index: 42 }] }", attribute.to_string()); + assert_eq!(expected, attribute.to_string()); let mut constant_pool = ConstantPool::default(); constant_pool.add(Constant::Utf8(attribute.name().to_string())); @@ -1282,11 +1333,12 @@ mod test { }], }; let expected_bytes = [0, 1, 0, 0, 0, 6, 0, 1, 0, 2, 0, 42]; + let expected = indoc! {" + LineNumberTable: + line 2: 42 + "}; - assert_eq!( - "LineNumberTable { name_index: 1, line_numbers: [LineNumber { start_pc: 2, line_number: 42 }] }", - attribute.to_string() - ); + assert_eq!(expected, attribute.to_string()); test_attribute(&attribute, &expected_bytes, &VERSION_45_3) } @@ -1683,7 +1735,7 @@ mod test { fn test_record() -> Result<()> { let constant = Attribute::ConstantValue { name_index: 1, - constantvalue_index: 42, + constant_value_index: 42, }; let record = Record { name_index: 2, @@ -1708,7 +1760,7 @@ mod test { assert!(!attribute.valid_for_version(&VERSION_45_0)); assert_eq!( - "Record { name_index: 4, records: [Record { name_index: 2, descriptor_index: 3, attributes: [ConstantValue { name_index: 1, constantvalue_index: 42 }] }] }", + "Record { name_index: 4, records: [Record { name_index: 2, descriptor_index: 3, attributes: [ConstantValue { name_index: 1, constant_value_index: 42 }] }] }", attribute.to_string() ); diff --git a/ristretto_classfile/src/attributes/method_parameter.rs b/ristretto_classfile/src/attributes/method_parameter.rs index b12f6d18..cdced144 100644 --- a/ristretto_classfile/src/attributes/method_parameter.rs +++ b/ristretto_classfile/src/attributes/method_parameter.rs @@ -42,7 +42,7 @@ impl fmt::Display for MethodParameter { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { write!( f, - "name_index: {}, access_flags: {}", + "name_index: #{}, access_flags: {}", self.name_index, self.access_flags ) } @@ -53,14 +53,15 @@ mod test { use super::*; #[test] - fn test_deserialization() -> Result<()> { - let expected_value = MethodParameter { + fn test_to_string() { + let method_parameter = MethodParameter { name_index: 3, access_flags: MethodAccessFlags::PUBLIC, }; - let mut bytes = Cursor::new(vec![0, 3, 0, 1]); - assert_eq!(expected_value, MethodParameter::from_bytes(&mut bytes)?); - Ok(()) + assert_eq!( + "name_index: #3, access_flags: (0x0001) ACC_PUBLIC", + method_parameter.to_string() + ); } #[test] diff --git a/ristretto_classfile/src/attributes/record.rs b/ristretto_classfile/src/attributes/record.rs index 669b9b63..d32e905a 100644 --- a/ristretto_classfile/src/attributes/record.rs +++ b/ristretto_classfile/src/attributes/record.rs @@ -75,7 +75,7 @@ mod test { fn test_to_string() { let attribute = Attribute::ConstantValue { name_index: 1, - constantvalue_index: 42, + constant_value_index: 42, }; let record = Record { name_index: 1, @@ -83,7 +83,7 @@ mod test { attributes: vec![attribute], }; assert_eq!( - "Record[name_index=1, descriptor_index=2, attributes=[ConstantValue { name_index: 1, constantvalue_index: 42 }]]", + "Record[name_index=1, descriptor_index=2, attributes=[ConstantValue { name_index: 1, constant_value_index: 42 }]]", record.to_string() ); } @@ -92,7 +92,7 @@ mod test { fn test_serialization() -> Result<()> { let attribute = Attribute::ConstantValue { name_index: 1, - constantvalue_index: 42, + constant_value_index: 42, }; let mut constant_pool = ConstantPool::default(); constant_pool.add(Constant::Utf8("ConstantValue".to_string())); diff --git a/ristretto_classfile/src/class_file.rs b/ristretto_classfile/src/class_file.rs index 5b951445..6bd811e9 100644 --- a/ristretto_classfile/src/class_file.rs +++ b/ristretto_classfile/src/class_file.rs @@ -50,18 +50,17 @@ impl ClassFile { /// /// # Errors /// Returns an error if the source file name is not found. - pub fn source_file(&self) -> Result<&String> { - let mut index = 0; + pub fn source_file(&self) -> Result> { for attribute in &self.attributes { if let Attribute::SourceFile { source_file_index, .. } = attribute { - index = *source_file_index; - break; + let source_file = self.constant_pool.try_get_utf8(*source_file_index)?; + return Ok(Some(source_file)); } } - self.constant_pool.try_get_utf8(index) + Ok(None) } /// Verify the `ClassFile`. @@ -181,11 +180,32 @@ impl ClassFile { impl fmt::Display for ClassFile { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + if let Some(source_file) = self.source_file().map_err(|_| fmt::Error)? { + writeln!(f, r#"Compiled from "{source_file}""#)?; + } writeln!(f, "version: {}", self.version)?; + writeln!(f, "major version: {}", self.version.major())?; + writeln!(f, "minor version: {}", self.version.minor())?; writeln!(f, "access_flags: {}", self.access_flags)?; writeln!(f, "this_class: #{}", self.this_class)?; writeln!(f, "super_class: #{}", self.super_class)?; - writeln!(f, "interfaces: {:?}", self.interfaces)?; + writeln!( + f, + "interfaces: {}, fields: {}, methods: {}, attributes: {}", + self.interfaces.len(), + self.fields.len(), + self.methods.len(), + self.attributes.len() + )?; + + writeln!(f, "Constant Pool:")?; + write!(f, "{}", self.constant_pool)?; + + writeln!(f, "interfaces:")?; + for interface in &self.interfaces { + writeln!(f, " #{interface}")?; + } + writeln!(f, "fields:")?; for (index, field) in self.fields.iter().enumerate() { if index > 0 { @@ -193,6 +213,7 @@ impl fmt::Display for ClassFile { } writeln!(f, "{}", indent_lines(&field.to_string(), " "))?; } + writeln!(f, "methods:")?; for (index, method) in self.methods.iter().enumerate() { if index > 0 { @@ -200,11 +221,9 @@ impl fmt::Display for ClassFile { } writeln!(f, "{}", indent_lines(&method.to_string(), " "))?; } + writeln!(f, "attributes:")?; - for (index, attribute) in self.attributes.iter().enumerate() { - if index > 0 { - writeln!(f)?; - } + for attribute in &self.attributes { writeln!(f, "{}", indent_lines(&attribute.to_string(), " "))?; } Ok(()) @@ -215,7 +234,6 @@ impl fmt::Display for ClassFile { mod test { use super::*; use crate::error::Result; - use crate::Error::InvalidConstantPoolIndex; use indoc::indoc; #[test] @@ -259,16 +277,19 @@ mod test { let class_bytes = include_bytes!("../classes/Simple.class"); let expected_bytes = class_bytes.to_vec(); let class_file = ClassFile::from_bytes(&mut Cursor::new(expected_bytes.clone()))?; + let source_file = class_file + .source_file()? + .map_or(String::new(), std::clone::Clone::clone); - assert_eq!("Simple.java", class_file.source_file()?); + assert_eq!("Simple.java", source_file); Ok(()) } #[test] - fn test_source_file_invalid_constant_pool() { + fn test_source_file_none() -> Result<()> { let class_file = ClassFile::default(); - - assert_eq!(Err(InvalidConstantPoolIndex(0)), class_file.source_file()); + assert_eq!(None, class_file.source_file()?); + Ok(()) } #[test] @@ -314,22 +335,45 @@ mod test { let class_bytes = include_bytes!("../classes/Minimum.class"); let expected_bytes = class_bytes.to_vec(); let class_file = ClassFile::from_bytes(&mut Cursor::new(expected_bytes.clone()))?; - let expected = indoc! {" + let expected = indoc! {r#" + Compiled from "Minimum.java" version: Java 21 + major version: 65 + minor version: 0 access_flags: (0x0021) ACC_PUBLIC, ACC_SUPER this_class: #7 super_class: #2 - interfaces: [] + interfaces: 0, fields: 0, methods: 1, attributes: 1 + Constant Pool: + #1 = MethodRef #2.#3 + #2 = Class #4 + #3 = NameAndType #5:#6 + #4 = Utf8 java/lang/Object + #5 = Utf8 + #6 = Utf8 ()V + #7 = Class #8 + #8 = Utf8 Minimum + #9 = Utf8 Code + #10 = Utf8 LineNumberTable + #11 = Utf8 SourceFile + #12 = Utf8 Minimum.java + interfaces: fields: methods: access_flags: (0x0001) ACC_PUBLIC name_index: #5 descriptor_index: #6 attributes: - Code { name_index: 9, max_stack: 1, max_locals: 1, code: [42, 183, 0, 1, 177], exceptions: [], attributes: [LineNumberTable { name_index: 10, line_numbers: [LineNumber { start_pc: 0, line_number: 1 }] }] } + Code: + max_stack = 1, max_locals = 1 + 0: aload_0 + 1: invokespecial #1 + 4: return + LineNumberTable: + line 0: 1 attributes: SourceFile { name_index: 11, source_file_index: 12 } - "}; + "#}; assert_eq!(expected, class_file.to_string()); Ok(()) diff --git a/ristretto_classfile/src/constant_pool.rs b/ristretto_classfile/src/constant_pool.rs index f14ec933..b51c0ec0 100644 --- a/ristretto_classfile/src/constant_pool.rs +++ b/ristretto_classfile/src/constant_pool.rs @@ -124,7 +124,7 @@ impl fmt::Display for ConstantEntry { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { match self { ConstantEntry::Constant(constant) => write!(f, "{constant}"), - ConstantEntry::Placeholder => write!(f, "Placeholder"), + ConstantEntry::Placeholder => Ok(()), } } } @@ -173,14 +173,21 @@ impl<'a> IntoIterator for &'a ConstantPool { } impl fmt::Display for ConstantPool { + // #1 = Class #2 // java/lang/Byte + // #2 = Utf8 java/lang/Byte + // #3 = Class #4 // java/lang/Integer + // #4 = Utf8 java/lang/Integer fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - for (index, constant) in self.constants.iter().enumerate() { - if matches!(constant, ConstantEntry::Placeholder) { - continue; + for (index, constant_entry) in self.constants.iter().enumerate() { + match constant_entry { + ConstantEntry::Constant(constant) => { + let index = index + 1; + let value = constant.to_string(); + let (name, value) = value.split_once(' ').unwrap_or_default(); + writeln!(f, "{:>5} = {name:<18} {value}", format!("#{index}"))?; + } + ConstantEntry::Placeholder => continue, } - - let index = index + 1; - writeln!(f, "{index}: {constant}")?; } Ok(()) } @@ -190,7 +197,15 @@ impl fmt::Display for ConstantPool { mod test { use super::*; use crate::constant::Constant; - use indoc::indoc; + + #[test] + fn test_constant_pool_entry_to_string() { + assert_eq!( + "Integer 42", + ConstantEntry::Constant(Constant::Integer(42)).to_string() + ); + assert_eq!("", ConstantEntry::Placeholder.to_string()); + } #[test] fn test_get_zero_none() { @@ -325,11 +340,7 @@ mod test { constant_pool.add(Constant::Utf8("foo".to_string())); constant_pool.add(Constant::Integer(42)); constant_pool.add(Constant::Long(1_234_567_890)); - let expected = indoc! {" - 1: Utf8 foo - 2: Integer 42 - 3: Long 1234567890 - "}; + let expected = " #1 = Utf8 foo\n #2 = Integer 42\n #3 = Long 1234567890\n"; assert_eq!(expected, constant_pool.to_string()); } diff --git a/ristretto_classfile/src/field.rs b/ristretto_classfile/src/field.rs index 3593ad36..cbb1486f 100644 --- a/ristretto_classfile/src/field.rs +++ b/ristretto_classfile/src/field.rs @@ -75,10 +75,7 @@ impl fmt::Display for Field { writeln!(f, "descriptor_index: #{}", self.descriptor_index)?; writeln!(f, "field_type: {:?}", self.field_type)?; writeln!(f, "attributes:")?; - for (index, attribute) in self.attributes.iter().enumerate() { - if index > 0 { - writeln!(f)?; - } + for attribute in &self.attributes { writeln!(f, "{}", indent_lines(&attribute.to_string(), " "))?; } Ok(()) @@ -115,7 +112,7 @@ mod test { descriptor_index: #2 field_type: Base(Int) attributes: - ConstantValue { name_index: 1, constantvalue_index: 1026 } + ConstantValue { name_index: 1, constant_value_index: 1026 } "}; assert_eq!(expected, field.to_string()); Ok(()) diff --git a/ristretto_classfile/src/method.rs b/ristretto_classfile/src/method.rs index 267f2531..cf17ae8b 100644 --- a/ristretto_classfile/src/method.rs +++ b/ristretto_classfile/src/method.rs @@ -68,10 +68,7 @@ impl fmt::Display for Method { writeln!(f, "name_index: #{}", self.name_index)?; writeln!(f, "descriptor_index: #{}", self.descriptor_index)?; writeln!(f, "attributes:")?; - for (index, attribute) in self.attributes.iter().enumerate() { - if index > 0 { - writeln!(f)?; - } + for attribute in &self.attributes { writeln!(f, "{}", indent_lines(&attribute.to_string(), " "))?; } Ok(()) @@ -86,26 +83,30 @@ mod test { use indoc::indoc; #[test] - fn test_to_string() -> Result<()> { - let mut constant_pool = ConstantPool::default(); - constant_pool.add(Constant::Utf8("ConstantValue".to_string())); - let mut attribute_bytes = Cursor::new([0, 1, 0, 0, 0, 2, 4, 2].to_vec()); - let attribute = Attribute::from_bytes(&constant_pool, &mut attribute_bytes)?; + fn test_to_string() { + let attribute1 = Attribute::ConstantValue { + name_index: 1, + constant_value_index: 2, + }; + let attribute2 = Attribute::ConstantValue { + name_index: 3, + constant_value_index: 4, + }; let method = Method { access_flags: MethodAccessFlags::PUBLIC, name_index: 1, descriptor_index: 2, - attributes: vec![attribute], + attributes: vec![attribute1, attribute2], }; let expected = indoc! {" access_flags: (0x0001) ACC_PUBLIC name_index: #1 descriptor_index: #2 attributes: - ConstantValue { name_index: 1, constantvalue_index: 1026 } + ConstantValue { name_index: 1, constant_value_index: 2 } + ConstantValue { name_index: 3, constant_value_index: 4 } "}; assert_eq!(expected, method.to_string()); - Ok(()) } #[test]