Workspace of 9 crates (noxis, noxis-core, noxis-classfile, noxis-analysis, noxis-passes, noxis-runtime, noxis-jar, noxis-verify, noxis-structure-check). AGPL-3.0-only license, Rust 2024 edition, unsafe_code denied.
152 lines
6.8 KiB
Rust
152 lines
6.8 KiB
Rust
//! Spike, часть 2: доступна ли **генерация** кадров `StackMapTable`.
|
||
//!
|
||
//! Ключевой вопрос `docs/stack.md` §3: считает ли `ristretto_classfile` кадры
|
||
//! сам и отдаёт ли их наружу. Если да — наша работа сводится к записи; если
|
||
//! нет — кадры придётся считать (или вендорить их `verifiers::bytecode`).
|
||
|
||
#![allow(clippy::expect_used, clippy::unwrap_used)]
|
||
|
||
use ristretto_classfile::attributes::{Attribute, Instruction, StackFrame};
|
||
use ristretto_classfile::verifiers::bytecode::{
|
||
FallbackStrategy, VerificationPath, VerifierConfig, verify_class,
|
||
};
|
||
use ristretto_classfile::verifiers::context::VerificationContext;
|
||
use ristretto_classfile::verifiers::error::Result as VerifyResult;
|
||
use ristretto_classfile::{
|
||
ClassAccessFlags, ClassFile, Constant, ConstantPool, MethodAccessFlags, Version,
|
||
};
|
||
|
||
/// Разрешающий контекст: в spike'е нас интересует только сам вывод типов.
|
||
#[derive(Debug)]
|
||
struct Permissive;
|
||
|
||
impl VerificationContext for Permissive {
|
||
fn is_subclass(&self, _subclass: &str, _superclass: &str) -> VerifyResult<bool> {
|
||
Ok(true)
|
||
}
|
||
|
||
fn is_assignable(&self, _target: &str, _source: &str) -> VerifyResult<bool> {
|
||
Ok(true)
|
||
}
|
||
|
||
fn common_superclass(&self, _class1: &str, _class2: &str) -> VerifyResult<String> {
|
||
Ok("java/lang/Object".to_string())
|
||
}
|
||
}
|
||
|
||
/// `void run()` с одной ветвью: на сдвиге 3 (`Ifeq`-target) кадр обязателен.
|
||
fn branch_class() -> ClassFile<'static> {
|
||
let mut constant_pool = ConstantPool::default();
|
||
let utf8_name = constant_pool
|
||
.add(Constant::utf8("demo/Branch"))
|
||
.expect("utf8");
|
||
let this_class = constant_pool
|
||
.add(Constant::Class(utf8_name))
|
||
.expect("class");
|
||
let run = constant_pool.add(Constant::utf8("run")).expect("utf8");
|
||
let descriptor = constant_pool.add(Constant::utf8("()V")).expect("utf8");
|
||
let code = constant_pool.add(Constant::utf8("Code")).expect("utf8");
|
||
|
||
ClassFile {
|
||
version: Version::Java8 { minor: 0 },
|
||
constant_pool,
|
||
access_flags: ClassAccessFlags::PUBLIC,
|
||
this_class,
|
||
super_class: 0,
|
||
interfaces: Vec::new(),
|
||
fields: Vec::new(),
|
||
methods: vec![ristretto_classfile::Method {
|
||
access_flags: MethodAccessFlags::PUBLIC | MethodAccessFlags::STATIC,
|
||
name_index: run,
|
||
descriptor_index: descriptor,
|
||
attributes: vec![Attribute::Code {
|
||
name_index: code,
|
||
max_stack: 1,
|
||
max_locals: 0,
|
||
code: vec![
|
||
Instruction::Iconst_1,
|
||
Instruction::Ifeq(3),
|
||
Instruction::Return,
|
||
Instruction::Return,
|
||
],
|
||
exception_table: Vec::new(),
|
||
attributes: Vec::new(),
|
||
}],
|
||
}],
|
||
attributes: Vec::new(),
|
||
code_source_url: None,
|
||
}
|
||
}
|
||
|
||
fn inference_config() -> VerifierConfig {
|
||
VerifierConfig::default().with_fallback_strategy(FallbackStrategy::AlwaysInference)
|
||
}
|
||
|
||
/// Без `StackMapTable` inference обязан пройти сам — значит, движок кадров у
|
||
/// крейта есть, просто его результат не отдаётся наружу.
|
||
#[test]
|
||
fn inference_path_verifies_without_stack_map_table() {
|
||
let class_file = branch_class();
|
||
let results = verify_class(&class_file, &Permissive, &inference_config()).expect("verify");
|
||
|
||
assert_eq!(results.len(), 1);
|
||
assert!(results[0].success, "ошибка: {:?}", results[0].error);
|
||
assert_eq!(results[0].path_used, VerificationPath::Inference);
|
||
}
|
||
|
||
/// Строгий режим без кадров обязан упасть — иначе требование `StackMapTable`
|
||
/// для классов ≥ 50 не проверяется вовсе.
|
||
#[test]
|
||
fn strict_mode_rejects_missing_stack_map_table() {
|
||
let class_file = branch_class();
|
||
let config = VerifierConfig::default().with_fallback_strategy(FallbackStrategy::Strict);
|
||
let outcome = verify_class(&class_file, &Permissive, &config);
|
||
|
||
// Строгий режим падает либо `Err`, либо `success == false` — оба варианта
|
||
// приемлемы, важен сам отказ.
|
||
let rejected = match &outcome {
|
||
Err(_) => true,
|
||
Ok(results) => results.iter().any(|r| !r.success),
|
||
};
|
||
assert!(rejected, "строгий режим обязан отвергнуть класс без кадров");
|
||
}
|
||
|
||
/// Негативный результат spike'а: кадр не на том сдвиге не отдаёт диагностику.
|
||
///
|
||
/// Ждали `Ok(vec![result])` с заполненным `diagnostic.expected` — тогда
|
||
/// генерация кадров шла бы итеративным путём через публичный API. Фактически
|
||
/// `verify_class` в строгом режиме возвращает `Err(VerifyError)`, и ожидаемый
|
||
/// кадр до нас не доходит: из крейта достаётся только место ошибки
|
||
/// («merge point at offset N»), но не её содержимое.
|
||
///
|
||
/// Вывод для `docs/stack.md` §3: проверять кадры крейт умеет, отдавать — нет.
|
||
#[test]
|
||
fn diagnostic_exposes_expected_frame_on_failure() {
|
||
let mut class_file = branch_class();
|
||
let name_index = class_file
|
||
.constant_pool
|
||
.add_utf8("StackMapTable")
|
||
.expect("utf8");
|
||
// Кадр на сдвиге 0, а настоящий якорь — на сдвиге 3.
|
||
class_file.attributes.push(Attribute::StackMapTable {
|
||
name_index,
|
||
frames: vec![StackFrame::SameFrame { frame_type: 0 }],
|
||
});
|
||
// Кладём атрибут в метод, а не в класс: `StackMapTable` живёт в `Code`.
|
||
let stack_map = class_file.attributes.pop().expect("frame");
|
||
match &mut class_file.methods[0].attributes[0] {
|
||
Attribute::Code { attributes, .. } => attributes.push(stack_map),
|
||
other => panic!("ожидался Code, получено {other:?}"),
|
||
}
|
||
|
||
let strict = VerifierConfig::default().with_fallback_strategy(FallbackStrategy::Strict);
|
||
let outcome = verify_class(&class_file, &Permissive, &strict);
|
||
|
||
// Наблюдаемое поведение: `Err`, а не `Ok` с `diagnostic`.
|
||
let err = outcome.expect_err("битые кадры обязаны быть отвергнуты");
|
||
let message = err.to_string();
|
||
assert!(
|
||
message.contains("StackMapTable"),
|
||
"ожидалась ошибка про кадры, получено: {message}"
|
||
);
|
||
}
|