classfile,passes,jar: model PermittedSubclasses, normalize interface flags, name class in write errors
This commit is contained in:
parent
e9d5e44b9e
commit
9a33cef9b1
11 changed files with 299 additions and 85 deletions
|
|
@ -47,6 +47,12 @@ pub(super) fn convert(attr: &RtAttr, pool: &ConstantPool<'_>) -> Result<Attribut
|
||||||
.map(|index| class_name(pool, *index))
|
.map(|index| class_name(pool, *index))
|
||||||
.collect::<Result<_, _>>()?,
|
.collect::<Result<_, _>>()?,
|
||||||
),
|
),
|
||||||
|
RtAttr::PermittedSubclasses { class_indexes, .. } => Attribute::PermittedSubclasses(
|
||||||
|
class_indexes
|
||||||
|
.iter()
|
||||||
|
.map(|index| class_name(pool, *index))
|
||||||
|
.collect::<Result<_, _>>()?,
|
||||||
|
),
|
||||||
RtAttr::EnclosingMethod {
|
RtAttr::EnclosingMethod {
|
||||||
class_index,
|
class_index,
|
||||||
method_index,
|
method_index,
|
||||||
|
|
|
||||||
|
|
@ -11,6 +11,21 @@ use super::pool::{class_name, utf8};
|
||||||
use crate::error::ClassfileError;
|
use crate::error::ClassfileError;
|
||||||
use crate::model::{Attribute, ClassFile, Code, Field, Method, Version};
|
use crate::model::{Attribute, ClassFile, Code, Field, Method, Version};
|
||||||
|
|
||||||
|
/// JVMS 4.1: an interface implies `ABSTRACT` and ignores `SUPER`. Some
|
||||||
|
/// compilers emit a bare `0x200` that HotSpot tolerates, but the strict
|
||||||
|
/// backend verifier rejects it on write (after widening, as `0x201`) - so
|
||||||
|
/// normalize once on parse. Interfaces are abstract by definition: no
|
||||||
|
/// behaviour changes.
|
||||||
|
fn normalize_class_flags(flags: u16) -> u16 {
|
||||||
|
const INTERFACE: u16 = 0x0200;
|
||||||
|
const SUPER: u16 = 0x0020;
|
||||||
|
const ABSTRACT: u16 = 0x0400;
|
||||||
|
if flags & INTERFACE == 0 {
|
||||||
|
return flags;
|
||||||
|
}
|
||||||
|
(flags | ABSTRACT) & !SUPER
|
||||||
|
}
|
||||||
|
|
||||||
/// Parse `.class` bytes into our IR.
|
/// Parse `.class` bytes into our IR.
|
||||||
pub(crate) fn parse_class(bytes: &[u8]) -> Result<ClassFile, ClassfileError> {
|
pub(crate) fn parse_class(bytes: &[u8]) -> Result<ClassFile, ClassfileError> {
|
||||||
let rt = RtClass::from_bytes(bytes).map_err(|e| ClassfileError::Parse(e.to_string()))?;
|
let rt = RtClass::from_bytes(bytes).map_err(|e| ClassfileError::Parse(e.to_string()))?;
|
||||||
|
|
@ -25,7 +40,7 @@ pub(crate) fn parse_class(bytes: &[u8]) -> Result<ClassFile, ClassfileError> {
|
||||||
minor: rt.version.minor(),
|
minor: rt.version.minor(),
|
||||||
major: rt.version.major(),
|
major: rt.version.major(),
|
||||||
},
|
},
|
||||||
access_flags: rt.access_flags.bits(),
|
access_flags: normalize_class_flags(rt.access_flags.bits()),
|
||||||
this_class: class_name(pool, rt.this_class)?,
|
this_class: class_name(pool, rt.this_class)?,
|
||||||
super_class: optional_class(pool, rt.super_class)?,
|
super_class: optional_class(pool, rt.super_class)?,
|
||||||
interfaces,
|
interfaces,
|
||||||
|
|
|
||||||
|
|
@ -145,6 +145,13 @@ pub(super) fn convert(attr: &Attribute, pool: &mut Pool) -> Result<RtAttr, Class
|
||||||
.map(|name| pool.class(name))
|
.map(|name| pool.class(name))
|
||||||
.collect::<Result<_, _>>()?,
|
.collect::<Result<_, _>>()?,
|
||||||
},
|
},
|
||||||
|
Attribute::PermittedSubclasses(classes) => RtAttr::PermittedSubclasses {
|
||||||
|
name_index: pool.utf8("PermittedSubclasses")?,
|
||||||
|
class_indexes: classes
|
||||||
|
.iter()
|
||||||
|
.map(|name| pool.class(name))
|
||||||
|
.collect::<Result<_, _>>()?,
|
||||||
|
},
|
||||||
Attribute::ConstantValue(value) => RtAttr::ConstantValue {
|
Attribute::ConstantValue(value) => RtAttr::ConstantValue {
|
||||||
name_index: pool.utf8("ConstantValue")?,
|
name_index: pool.utf8("ConstantValue")?,
|
||||||
constant_value_index: pool.constant(value)?,
|
constant_value_index: pool.constant(value)?,
|
||||||
|
|
|
||||||
|
|
@ -23,6 +23,9 @@ pub enum Attribute {
|
||||||
NestHost(String),
|
NestHost(String),
|
||||||
/// `NestMembers`: the member classes of this nest host (JVMS §4.7.29).
|
/// `NestMembers`: the member classes of this nest host (JVMS §4.7.29).
|
||||||
NestMembers(Vec<String>),
|
NestMembers(Vec<String>),
|
||||||
|
/// `PermittedSubclasses`: who may extend a sealed class (JVMS §4.7.31).
|
||||||
|
/// Stored as names: pool indices go stale when the pool is rebuilt.
|
||||||
|
PermittedSubclasses(Vec<String>),
|
||||||
/// A field's `ConstantValue` (`static final`): a value from the pool.
|
/// A field's `ConstantValue` (`static final`): a value from the pool.
|
||||||
ConstantValue(Const),
|
ConstantValue(Const),
|
||||||
/// `BootstrapMethods` for `invokedynamic` (lambdas): method + arguments.
|
/// `BootstrapMethods` for `invokedynamic` (lambdas): method + arguments.
|
||||||
|
|
|
||||||
146
crates/noxis-classfile/tests/roundtrip/attrs.rs
Normal file
146
crates/noxis-classfile/tests/roundtrip/attrs.rs
Normal file
|
|
@ -0,0 +1,146 @@
|
||||||
|
//! Attribute round trips: bootstrap methods, nesting, flags, sealing.
|
||||||
|
|
||||||
|
#![allow(clippy::expect_used, clippy::unwrap_used)]
|
||||||
|
|
||||||
|
use super::round_trip;
|
||||||
|
use ristretto_classfile::attributes::{
|
||||||
|
Attribute as RtAttr, BootstrapMethod as RtBsm, Instruction as Rt,
|
||||||
|
};
|
||||||
|
use ristretto_classfile::{
|
||||||
|
ClassAccessFlags, ClassFile as RtClass, ConstantPool, JAVA_21, Method as RtMethod,
|
||||||
|
MethodAccessFlags, ReferenceKind,
|
||||||
|
};
|
||||||
|
/// `BootstrapMethods` + `invokedynamic` survive the round trip.
|
||||||
|
#[test]
|
||||||
|
fn bootstrap_methods_round_trip() {
|
||||||
|
let mut pool = ConstantPool::default();
|
||||||
|
let this = pool.add_class("demo/Lambda").expect("add_class");
|
||||||
|
let object = pool.add_class("java/lang/Object").expect("add_object");
|
||||||
|
let owner = pool.add_class("demo/RT").expect("owner");
|
||||||
|
let method_ref = pool.add_method_ref(owner, "boot", "()V").expect("ref");
|
||||||
|
let handle = pool
|
||||||
|
.add_method_handle(ReferenceKind::InvokeStatic, method_ref)
|
||||||
|
.expect("handle");
|
||||||
|
let arg = pool.add_string("x").expect("arg");
|
||||||
|
let bsm_name = pool.add_utf8("BootstrapMethods").expect("attr");
|
||||||
|
let name = pool.add_utf8("run").expect("name");
|
||||||
|
let desc = pool.add_utf8("()V").expect("desc");
|
||||||
|
let code_name = pool.add_utf8("Code").expect("attr");
|
||||||
|
let dynamic = pool.add_invoke_dynamic(0, "run", "()V").expect("indy");
|
||||||
|
|
||||||
|
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![RtAttr::Code {
|
||||||
|
name_index: code_name,
|
||||||
|
max_stack: 1,
|
||||||
|
max_locals: 0,
|
||||||
|
code: vec![Rt::Invokedynamic(dynamic), Rt::Return],
|
||||||
|
exception_table: vec![],
|
||||||
|
attributes: vec![],
|
||||||
|
}],
|
||||||
|
}],
|
||||||
|
attributes: vec![RtAttr::BootstrapMethods {
|
||||||
|
name_index: bsm_name,
|
||||||
|
methods: vec![RtBsm {
|
||||||
|
bootstrap_method_ref: handle,
|
||||||
|
arguments: vec![arg],
|
||||||
|
}],
|
||||||
|
}],
|
||||||
|
..RtClass::default()
|
||||||
|
};
|
||||||
|
|
||||||
|
let ir = round_trip(&rt);
|
||||||
|
assert_eq!(ir.attributes.len(), 1);
|
||||||
|
assert!(format!("{:?}", ir.attributes[0]).contains("BootstrapMethods"));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// `NestMembers` переживает пересборку пула (баг: `ClassFormatError`).
|
||||||
|
#[test]
|
||||||
|
fn nest_attributes_round_trip() {
|
||||||
|
let mut pool = ConstantPool::default();
|
||||||
|
let this = pool.add_class("demo/Nest").expect("c");
|
||||||
|
let object = pool.add_class("java/lang/Object").expect("o");
|
||||||
|
let member = pool.add_class("demo/Nest$Inner").expect("m");
|
||||||
|
let mn = pool.add_utf8("NestMembers").expect("n");
|
||||||
|
let mems = RtAttr::NestMembers {
|
||||||
|
name_index: mn,
|
||||||
|
class_indexes: vec![this, member],
|
||||||
|
};
|
||||||
|
let rt = RtClass {
|
||||||
|
version: JAVA_21,
|
||||||
|
constant_pool: pool,
|
||||||
|
this_class: this,
|
||||||
|
super_class: object,
|
||||||
|
attributes: vec![mems],
|
||||||
|
..RtClass::default()
|
||||||
|
};
|
||||||
|
let ir = round_trip(&rt);
|
||||||
|
assert!(format!("{:?}", ir.attributes).contains("NestMembers"));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// `parse → write → parse` даёт тот же IR; записанное верифицируется.
|
||||||
|
#[test]
|
||||||
|
fn bare_interface_flags_normalize_on_parse() {
|
||||||
|
use ristretto_classfile::ClassAccessFlags;
|
||||||
|
let mut pool = ConstantPool::default();
|
||||||
|
let this = pool.add_class("demo/Bare").expect("add_class");
|
||||||
|
let object = pool.add_class("java/lang/Object").expect("add_object");
|
||||||
|
let rt = RtClass {
|
||||||
|
version: JAVA_21,
|
||||||
|
constant_pool: pool,
|
||||||
|
access_flags: ClassAccessFlags::INTERFACE,
|
||||||
|
this_class: this,
|
||||||
|
super_class: object,
|
||||||
|
interfaces: vec![],
|
||||||
|
fields: vec![],
|
||||||
|
methods: vec![],
|
||||||
|
attributes: vec![],
|
||||||
|
..RtClass::default()
|
||||||
|
};
|
||||||
|
let ir = round_trip(&rt);
|
||||||
|
assert_eq!(ir.access_flags & 0x0200, 0x0200, "still an interface");
|
||||||
|
assert_eq!(
|
||||||
|
ir.access_flags & 0x0400,
|
||||||
|
0x0400,
|
||||||
|
"abstract is implied for interfaces"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// `PermittedSubclasses` survives the round trip: its pool indices are
|
||||||
|
/// remapped on write (not copied verbatim), or the backend rejects the
|
||||||
|
/// rebuilt pool (`bad constant type`).
|
||||||
|
#[test]
|
||||||
|
fn permitted_subclasses_round_trip() {
|
||||||
|
let mut pool = ConstantPool::default();
|
||||||
|
let this = pool.add_class("demo/Sealed").expect("add_class");
|
||||||
|
let object = pool.add_class("java/lang/Object").expect("add_object");
|
||||||
|
let permitted = pool.add_class("demo/Impl").expect("add permitted");
|
||||||
|
let attr_name = pool.add_utf8("PermittedSubclasses").expect("attr");
|
||||||
|
let rt = RtClass {
|
||||||
|
version: JAVA_21,
|
||||||
|
constant_pool: pool,
|
||||||
|
access_flags: ClassAccessFlags::PUBLIC | ClassAccessFlags::ABSTRACT,
|
||||||
|
this_class: this,
|
||||||
|
super_class: object,
|
||||||
|
interfaces: vec![],
|
||||||
|
fields: vec![],
|
||||||
|
methods: vec![],
|
||||||
|
attributes: vec![RtAttr::PermittedSubclasses {
|
||||||
|
name_index: attr_name,
|
||||||
|
class_indexes: vec![permitted],
|
||||||
|
}],
|
||||||
|
..RtClass::default()
|
||||||
|
};
|
||||||
|
let ir = round_trip(&rt);
|
||||||
|
assert_eq!(ir.this_class, "demo/Sealed");
|
||||||
|
}
|
||||||
|
|
@ -8,12 +8,12 @@
|
||||||
|
|
||||||
use noxis_classfile::{ClassFile, format};
|
use noxis_classfile::{ClassFile, format};
|
||||||
use ristretto_classfile::attributes::{
|
use ristretto_classfile::attributes::{
|
||||||
Attribute as RtAttr, BootstrapMethod as RtBsm, ExceptionTableEntry as RtEntry,
|
Attribute as RtAttr, ExceptionTableEntry as RtEntry, Instruction as Rt, LineNumber as RtLine,
|
||||||
Instruction as Rt, LineNumber as RtLine, StackFrame as RtFrame,
|
StackFrame as RtFrame,
|
||||||
};
|
};
|
||||||
use ristretto_classfile::{
|
use ristretto_classfile::{
|
||||||
ClassAccessFlags, ClassFile as RtClass, ConstantPool, Field as RtField, FieldAccessFlags,
|
ClassAccessFlags, ClassFile as RtClass, ConstantPool, Field as RtField, FieldAccessFlags,
|
||||||
FieldType, JAVA_21, Method as RtMethod, MethodAccessFlags, ReferenceKind,
|
FieldType, JAVA_21, Method as RtMethod, MethodAccessFlags,
|
||||||
};
|
};
|
||||||
|
|
||||||
/// Минимальный класс: версия, флаги, поле с `ConstantValue`, `SourceFile`.
|
/// Минимальный класс: версия, флаги, поле с `ConstantValue`, `SourceFile`.
|
||||||
|
|
@ -151,85 +151,6 @@ fn code_with_branches_round_trip() {
|
||||||
assert_eq!(code.attributes.len(), 1);
|
assert_eq!(code.attributes.len(), 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// `BootstrapMethods` + `invokedynamic` переживают round-trip.
|
|
||||||
#[test]
|
|
||||||
fn bootstrap_methods_round_trip() {
|
|
||||||
let mut pool = ConstantPool::default();
|
|
||||||
let this = pool.add_class("demo/Lambda").expect("add_class");
|
|
||||||
let object = pool.add_class("java/lang/Object").expect("add_object");
|
|
||||||
let owner = pool.add_class("demo/RT").expect("owner");
|
|
||||||
let method_ref = pool.add_method_ref(owner, "boot", "()V").expect("ref");
|
|
||||||
let handle = pool
|
|
||||||
.add_method_handle(ReferenceKind::InvokeStatic, method_ref)
|
|
||||||
.expect("handle");
|
|
||||||
let arg = pool.add_string("x").expect("arg");
|
|
||||||
let bsm_name = pool.add_utf8("BootstrapMethods").expect("attr");
|
|
||||||
let name = pool.add_utf8("run").expect("name");
|
|
||||||
let desc = pool.add_utf8("()V").expect("desc");
|
|
||||||
let code_name = pool.add_utf8("Code").expect("attr");
|
|
||||||
let dynamic = pool.add_invoke_dynamic(0, "run", "()V").expect("indy");
|
|
||||||
|
|
||||||
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![RtAttr::Code {
|
|
||||||
name_index: code_name,
|
|
||||||
max_stack: 1,
|
|
||||||
max_locals: 0,
|
|
||||||
code: vec![Rt::Invokedynamic(dynamic), Rt::Return],
|
|
||||||
exception_table: vec![],
|
|
||||||
attributes: vec![],
|
|
||||||
}],
|
|
||||||
}],
|
|
||||||
attributes: vec![RtAttr::BootstrapMethods {
|
|
||||||
name_index: bsm_name,
|
|
||||||
methods: vec![RtBsm {
|
|
||||||
bootstrap_method_ref: handle,
|
|
||||||
arguments: vec![arg],
|
|
||||||
}],
|
|
||||||
}],
|
|
||||||
..RtClass::default()
|
|
||||||
};
|
|
||||||
|
|
||||||
let ir = round_trip(&rt);
|
|
||||||
assert_eq!(ir.attributes.len(), 1);
|
|
||||||
assert!(format!("{:?}", ir.attributes[0]).contains("BootstrapMethods"));
|
|
||||||
}
|
|
||||||
|
|
||||||
/// `NestMembers` переживает пересборку пула (баг: `ClassFormatError`).
|
|
||||||
#[test]
|
|
||||||
fn nest_attributes_round_trip() {
|
|
||||||
let mut pool = ConstantPool::default();
|
|
||||||
let this = pool.add_class("demo/Nest").expect("c");
|
|
||||||
let object = pool.add_class("java/lang/Object").expect("o");
|
|
||||||
let member = pool.add_class("demo/Nest$Inner").expect("m");
|
|
||||||
let mn = pool.add_utf8("NestMembers").expect("n");
|
|
||||||
let mems = RtAttr::NestMembers {
|
|
||||||
name_index: mn,
|
|
||||||
class_indexes: vec![this, member],
|
|
||||||
};
|
|
||||||
let rt = RtClass {
|
|
||||||
version: JAVA_21,
|
|
||||||
constant_pool: pool,
|
|
||||||
this_class: this,
|
|
||||||
super_class: object,
|
|
||||||
attributes: vec![mems],
|
|
||||||
..RtClass::default()
|
|
||||||
};
|
|
||||||
let ir = round_trip(&rt);
|
|
||||||
assert!(format!("{:?}", ir.attributes).contains("NestMembers"));
|
|
||||||
}
|
|
||||||
|
|
||||||
/// `parse → write → parse` даёт тот же IR; записанное верифицируется.
|
|
||||||
fn round_trip(rt: &RtClass<'_>) -> ClassFile {
|
fn round_trip(rt: &RtClass<'_>) -> ClassFile {
|
||||||
let mut original = Vec::new();
|
let mut original = Vec::new();
|
||||||
rt.to_bytes(&mut original).expect("to_bytes");
|
rt.to_bytes(&mut original).expect("to_bytes");
|
||||||
|
|
@ -243,3 +164,8 @@ fn round_trip(rt: &RtClass<'_>) -> ClassFile {
|
||||||
assert_eq!(first, second, "IR обязан пережить round-trip");
|
assert_eq!(first, second, "IR обязан пережить round-trip");
|
||||||
second
|
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;
|
||||||
|
|
@ -46,7 +46,13 @@ pub fn save(program: &Program) -> Result<Vec<Entry>, JarError> {
|
||||||
|
|
||||||
for class in &program.classes {
|
for class in &program.classes {
|
||||||
let name = format!("{}.class", class.this_class);
|
let name = format!("{}.class", class.this_class);
|
||||||
entries.push(Entry::new(name, format::write(class)?));
|
let bytes = format::write(class).map_err(|error| {
|
||||||
|
JarError::Classfile(noxis_classfile::ClassfileError::Write(format!(
|
||||||
|
"{}: {error}",
|
||||||
|
class.this_class
|
||||||
|
)))
|
||||||
|
})?;
|
||||||
|
entries.push(Entry::new(name, bytes));
|
||||||
}
|
}
|
||||||
for resource in &program.resources {
|
for resource in &program.resources {
|
||||||
entries.push(Entry::new(resource.path.clone(), resource.data.clone()));
|
entries.push(Entry::new(resource.path.clone(), resource.data.clone()));
|
||||||
|
|
|
||||||
|
|
@ -134,6 +134,13 @@ fn rewrite_attributes(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
Attribute::PermittedSubclasses(classes) => {
|
||||||
|
for name in classes.iter_mut() {
|
||||||
|
if let Some(mapped) = maps.classes.get(name.as_str()) {
|
||||||
|
*name = mapped.clone();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Attribute::LocalVariableTable(vars) => {
|
Attribute::LocalVariableTable(vars) => {
|
||||||
for var in vars.iter_mut() {
|
for var in vars.iter_mut() {
|
||||||
var.descriptor = rewrite_descriptor(&var.descriptor, &maps.classes);
|
var.descriptor = rewrite_descriptor(&var.descriptor, &maps.classes);
|
||||||
|
|
|
||||||
|
|
@ -109,3 +109,33 @@ fn generic_signatures_rewrite_all_references() {
|
||||||
&format!("Ljava/lang/Object;Ljava/lang/Comparable<L{helper};>;")
|
&format!("Ljava/lang/Object;Ljava/lang/Comparable<L{helper};>;")
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Sealed classes keep working: `PermittedSubclasses` entries follow the
|
||||||
|
/// mapping (a stale entry breaks loading with `ClassFormatError`, and raw
|
||||||
|
/// pool indices would go stale on rebuild anyway).
|
||||||
|
#[test]
|
||||||
|
fn permitted_subclasses_follow_the_mapping() {
|
||||||
|
use noxis_classfile::{Attribute, ClassBuilder, Version};
|
||||||
|
let mut program = sample();
|
||||||
|
program.classes.push(
|
||||||
|
ClassBuilder::new("app/Sealed", Some("java/lang/Object"))
|
||||||
|
.version(Version::JAVA_21)
|
||||||
|
.attribute(Attribute::PermittedSubclasses(vec![
|
||||||
|
"app/Helper".to_string(),
|
||||||
|
]))
|
||||||
|
.build(),
|
||||||
|
);
|
||||||
|
let ctx = run_rename(&mut program);
|
||||||
|
let helper = ctx.mapping.class("app/Helper").expect("renamed");
|
||||||
|
let sealed_name = ctx.mapping.class("app/Sealed").expect("renamed");
|
||||||
|
let sealed = program.class(sealed_name).expect("sealed");
|
||||||
|
let permitted = sealed
|
||||||
|
.attributes
|
||||||
|
.iter()
|
||||||
|
.find_map(|attr| match attr {
|
||||||
|
Attribute::PermittedSubclasses(names) => Some(names),
|
||||||
|
_ => None,
|
||||||
|
})
|
||||||
|
.expect("permitted");
|
||||||
|
assert_eq!(permitted, &vec![helper.to_string()]);
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -16,5 +16,6 @@ pub use java::{
|
||||||
};
|
};
|
||||||
pub use leaks::{RED_PATTERNS, SecretHit, check_patterns, collect_names, find_leaks, scan_secrets};
|
pub use leaks::{RED_PATTERNS, SecretHit, check_patterns, collect_names, find_leaks, scan_secrets};
|
||||||
pub use metrics::{
|
pub use metrics::{
|
||||||
ProgramStats, check_inflation, inflation_ratio, mappings_changed, program_stats,
|
ProgramStats, RunOutput, check_inflation, check_slowdown, inflation_ratio, mappings_changed,
|
||||||
|
measure_runtime, program_stats, run_java, slowdown_ratio,
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -79,6 +79,62 @@ pub fn mappings_changed(before: &Mapping, after: &Mapping) -> bool {
|
||||||
before != after
|
before != after
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Wall time of `java -cp <jar> <main_class> <args>` (stderr inherited).
|
||||||
|
pub fn measure_runtime(
|
||||||
|
jar: &std::path::Path,
|
||||||
|
main_class: &str,
|
||||||
|
args: &[&str],
|
||||||
|
) -> Option<std::time::Duration> {
|
||||||
|
run_java(jar, main_class, args).map(|run| run.elapsed)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// One `java` run: wall time plus captured stdout (for checksum comparison).
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub struct RunOutput {
|
||||||
|
pub elapsed: std::time::Duration,
|
||||||
|
pub stdout: Vec<u8>,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Run `java -cp <jar> <main_class> <args>`, capturing stdout.
|
||||||
|
/// `None` when `java` is missing, refuses to start, or the run fails:
|
||||||
|
/// a workload that cannot run has no budget to check.
|
||||||
|
pub fn run_java(jar: &std::path::Path, main_class: &str, args: &[&str]) -> Option<RunOutput> {
|
||||||
|
let start = std::time::Instant::now();
|
||||||
|
let output = std::process::Command::new("java")
|
||||||
|
.arg("-cp")
|
||||||
|
.arg(jar)
|
||||||
|
.arg(main_class)
|
||||||
|
.args(args)
|
||||||
|
.stderr(std::process::Stdio::null())
|
||||||
|
.output()
|
||||||
|
.ok()?;
|
||||||
|
output.status.success().then(|| RunOutput {
|
||||||
|
elapsed: start.elapsed(),
|
||||||
|
stdout: output.stdout,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Slowdown factor (`after / before`).
|
||||||
|
pub fn slowdown_ratio(before: std::time::Duration, after: std::time::Duration) -> f64 {
|
||||||
|
if before.is_zero() {
|
||||||
|
return f64::INFINITY;
|
||||||
|
}
|
||||||
|
after.as_secs_f64() / before.as_secs_f64()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Runtime within budget (`max_ratio`, e.g. `5.0` = at most five times slower).
|
||||||
|
pub fn check_slowdown(
|
||||||
|
before: std::time::Duration,
|
||||||
|
after: std::time::Duration,
|
||||||
|
max_ratio: f64,
|
||||||
|
) -> Result<(), String> {
|
||||||
|
let ratio = slowdown_ratio(before, after);
|
||||||
|
if ratio > max_ratio {
|
||||||
|
return Err(format!("slowdown {ratio:.2} over budget {max_ratio:.2}"));
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
#![allow(clippy::expect_used, clippy::unwrap_used)]
|
#![allow(clippy::expect_used, clippy::unwrap_used)]
|
||||||
|
|
@ -100,6 +156,17 @@ mod tests {
|
||||||
assert!(check_inflation(100, 200, 1.5).is_err());
|
assert!(check_inflation(100, 200, 1.5).is_err());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn slowdown_math() {
|
||||||
|
use std::time::Duration;
|
||||||
|
assert!(
|
||||||
|
(slowdown_ratio(Duration::from_secs(2), Duration::from_secs(3)) - 1.5).abs() < 1e-9
|
||||||
|
);
|
||||||
|
assert!(slowdown_ratio(Duration::ZERO, Duration::from_secs(1)).is_infinite());
|
||||||
|
assert!(check_slowdown(Duration::from_secs(2), Duration::from_secs(4), 5.0).is_ok());
|
||||||
|
assert!(check_slowdown(Duration::from_secs(2), Duration::from_secs(11), 5.0).is_err());
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn identical_mappings_are_stable() {
|
fn identical_mappings_are_stable() {
|
||||||
let mapping = Mapping::default();
|
let mapping = Mapping::default();
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue