Initial commit: noxis JVM bytecode obfuscator
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.
This commit is contained in:
commit
a641e69455
140 changed files with 17197 additions and 0 deletions
BIN
crates/noxis-classfile/tests/data/GuiItemConfig.class
Normal file
BIN
crates/noxis-classfile/tests/data/GuiItemConfig.class
Normal file
Binary file not shown.
BIN
crates/noxis-classfile/tests/data/MessageUtil.class
Normal file
BIN
crates/noxis-classfile/tests/data/MessageUtil.class
Normal file
Binary file not shown.
BIN
crates/noxis-classfile/tests/data/PaginationUtil.class
Normal file
BIN
crates/noxis-classfile/tests/data/PaginationUtil.class
Normal file
Binary file not shown.
BIN
crates/noxis-classfile/tests/data/TimeUtil.class
Normal file
BIN
crates/noxis-classfile/tests/data/TimeUtil.class
Normal file
Binary file not shown.
134
crates/noxis-classfile/tests/frames.rs
Normal file
134
crates/noxis-classfile/tests/frames.rs
Normal file
|
|
@ -0,0 +1,134 @@
|
|||
//! Приёмка `compute_frames` на фикстурах (`tests/data`).
|
||||
//!
|
||||
//! Кадры сносятся и считаются заново. Судьи: детерминизм + round-trip IR +
|
||||
//! настоящий `java -Xverify:all` для классов без внешних зависимостей.
|
||||
//! (`verify_class` backend'а для приёмки не годится: он ждёт кадры в байтах,
|
||||
//! а `from_bytes` отдаёт в индексах — см. `todo.md`.)
|
||||
|
||||
#![allow(clippy::expect_used, clippy::unwrap_used)]
|
||||
|
||||
use std::collections::BTreeMap;
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::process::Command;
|
||||
|
||||
use noxis_classfile::{Hierarchy, compute_frames, format};
|
||||
|
||||
/// Карта `класс → суперкласс` для LUB.
|
||||
#[derive(Default)]
|
||||
struct MapHierarchy {
|
||||
supers: BTreeMap<String, String>,
|
||||
}
|
||||
|
||||
impl Hierarchy for MapHierarchy {
|
||||
fn superclass(&self, class: &str) -> Option<String> {
|
||||
self.supers.get(class).cloned()
|
||||
}
|
||||
}
|
||||
|
||||
/// Классы только на JDK — их `java` проверит полностью.
|
||||
const JDK_ONLY: [&str; 2] = [
|
||||
"dev/loki/lorep/util/TimeUtil",
|
||||
"dev/loki/lorep/util/PaginationUtil",
|
||||
];
|
||||
|
||||
/// Фикстуры: снести кадры, посчитать заново, проверить выход.
|
||||
#[test]
|
||||
fn fixtures_recompute_and_verify() {
|
||||
let dir = PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("tests/data");
|
||||
let mut names: Vec<PathBuf> = std::fs::read_dir(&dir)
|
||||
.expect("tests/data")
|
||||
.filter_map(|entry| entry.ok().map(|entry| entry.path()))
|
||||
.filter(|path| path.extension().is_some_and(|ext| ext == "class"))
|
||||
.collect();
|
||||
names.sort();
|
||||
assert!(!names.is_empty(), "нет фикстур в tests/data");
|
||||
|
||||
let mut supers = BTreeMap::new();
|
||||
let mut parsed = Vec::new();
|
||||
for name in &names {
|
||||
let class = format::parse(&std::fs::read(name).expect("read")).expect("parse");
|
||||
supers.insert(class.this_class.clone(), class.super_class.clone());
|
||||
parsed.push(class);
|
||||
}
|
||||
// Без суперкласса — только `java/lang/Object`.
|
||||
supers.entry("java/lang/Object".to_string()).or_insert(None);
|
||||
let hierarchy = MapHierarchy {
|
||||
supers: supers
|
||||
.into_iter()
|
||||
.filter_map(|(k, v)| v.map(|sup| (k, sup)))
|
||||
.collect(),
|
||||
};
|
||||
|
||||
let scratch = std::env::temp_dir().join(format!("noxis-frames-{}", std::process::id()));
|
||||
let java = java_available();
|
||||
for class in &mut parsed {
|
||||
if class.version.major < 50 {
|
||||
continue;
|
||||
}
|
||||
for method in &mut class.methods {
|
||||
if method.code.is_none() {
|
||||
continue;
|
||||
}
|
||||
let first = compute_frames(&class.this_class, method, &hierarchy).expect("recompute");
|
||||
method.code.as_mut().expect("code").frames = first.clone();
|
||||
// Детерминизм: повторный прогон даёт те же кадры.
|
||||
let second = compute_frames(&class.this_class, method, &hierarchy).expect("recompute");
|
||||
assert_eq!(first, second, "недетерминированные кадры");
|
||||
}
|
||||
// Round-trip: записанное читается в тот же IR (включая кадры).
|
||||
let written = format::write(class).expect("write");
|
||||
assert_eq!(
|
||||
format::parse(&written).expect("re-parse"),
|
||||
*class,
|
||||
"round-trip"
|
||||
);
|
||||
if java && JDK_ONLY.contains(&class.this_class.as_str()) {
|
||||
java_verify(&scratch, class, &written).expect("java -Xverify:all");
|
||||
}
|
||||
}
|
||||
let _ = std::fs::remove_dir_all(&scratch);
|
||||
}
|
||||
|
||||
/// Есть ли `java` для проверки настоящим верификатором.
|
||||
fn java_available() -> bool {
|
||||
Command::new("java")
|
||||
.arg("-version")
|
||||
.output()
|
||||
.is_ok_and(|out| out.status.success())
|
||||
}
|
||||
|
||||
/// Записать класс в дерево пакетов и прогнать `java -Xverify:all`.
|
||||
/// Успех — загрузка без ошибок верификации (`main` при этом отсутствует).
|
||||
fn java_verify(
|
||||
scratch: &Path,
|
||||
class: &noxis_classfile::ClassFile,
|
||||
bytes: &[u8],
|
||||
) -> Result<(), String> {
|
||||
let mut path = scratch.to_path_buf();
|
||||
for part in class.this_class.split('/') {
|
||||
path.push(part);
|
||||
}
|
||||
path.set_extension("class");
|
||||
std::fs::create_dir_all(path.parent().expect("parent")).map_err(|e| format!("mkdir: {e}"))?;
|
||||
std::fs::write(&path, bytes).map_err(|e| format!("write: {e}"))?;
|
||||
let dotted = class.this_class.replace('/', ".");
|
||||
let out = Command::new("java")
|
||||
.arg("-Xverify:all")
|
||||
.arg("-cp")
|
||||
.arg(scratch)
|
||||
.arg(&dotted)
|
||||
.output()
|
||||
.map_err(|e| format!("запуск java: {e}"))?;
|
||||
let stderr = String::from_utf8_lossy(&out.stderr);
|
||||
for bad in [
|
||||
"VerifyError",
|
||||
"ClassFormatError",
|
||||
"NoClassDefFoundError",
|
||||
"LinkageError",
|
||||
] {
|
||||
if stderr.contains(bad) {
|
||||
return Err(format!("{dotted}:\n{stderr}"));
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
221
crates/noxis-classfile/tests/roundtrip.rs
Normal file
221
crates/noxis-classfile/tests/roundtrip.rs
Normal file
|
|
@ -0,0 +1,221 @@
|
|||
//! Round-trip `parse → write → parse` через backend.
|
||||
//!
|
||||
//! Пул при записи строится заново (порядок другой), поэтому сравнивается IR,
|
||||
//! а не байты. Записанные байты обязаны проходить структурный `verify`
|
||||
//! backend'а — это ловит битые ссылки пула и кривые атрибуты.
|
||||
|
||||
#![allow(clippy::expect_used, clippy::unwrap_used)]
|
||||
|
||||
use ristretto_classfile::attributes::{
|
||||
Attribute as RtAttr, BootstrapMethod as RtBsm, ExceptionTableEntry as RtEntry,
|
||||
Instruction as Rt, LineNumber as RtLine, StackFrame as RtFrame,
|
||||
};
|
||||
use ristretto_classfile::{
|
||||
ClassAccessFlags, ClassFile as RtClass, ConstantPool, Field as RtField, FieldAccessFlags,
|
||||
FieldType, JAVA_21, Method as RtMethod, MethodAccessFlags, ReferenceKind,
|
||||
};
|
||||
use noxis_classfile::{ClassFile, format};
|
||||
|
||||
/// Минимальный класс: версия, флаги, поле с `ConstantValue`, `SourceFile`.
|
||||
#[test]
|
||||
fn minimal_class_round_trip() {
|
||||
let mut pool = ConstantPool::default();
|
||||
let this = pool.add_class("demo/Round").expect("add_class");
|
||||
let object = pool.add_class("java/lang/Object").expect("add_object");
|
||||
let name = pool.add_utf8("K").expect("name");
|
||||
let desc = pool.add_utf8("I").expect("desc");
|
||||
let value = pool.add_integer(42).expect("value");
|
||||
let source_name = pool.add_utf8("SourceFile").expect("attr");
|
||||
let source_file = pool.add_utf8("Round.java").expect("file");
|
||||
let constant_name = pool.add_utf8("ConstantValue").expect("attr");
|
||||
|
||||
let rt = RtClass {
|
||||
version: JAVA_21,
|
||||
constant_pool: pool,
|
||||
access_flags: ClassAccessFlags::PUBLIC,
|
||||
this_class: this,
|
||||
super_class: object,
|
||||
interfaces: vec![],
|
||||
fields: vec![RtField {
|
||||
access_flags: FieldAccessFlags::PUBLIC
|
||||
| FieldAccessFlags::STATIC
|
||||
| FieldAccessFlags::FINAL,
|
||||
name_index: name,
|
||||
descriptor_index: desc,
|
||||
field_type: FieldType::parse("I").expect("field type"),
|
||||
attributes: vec![RtAttr::ConstantValue {
|
||||
name_index: constant_name,
|
||||
constant_value_index: value,
|
||||
}],
|
||||
}],
|
||||
methods: vec![],
|
||||
attributes: vec![RtAttr::SourceFile {
|
||||
name_index: source_name,
|
||||
source_file_index: source_file,
|
||||
}],
|
||||
..RtClass::default()
|
||||
};
|
||||
|
||||
let ir = round_trip(&rt);
|
||||
assert_eq!(ir.this_class, "demo/Round");
|
||||
assert_eq!(ir.super_class.as_deref(), Some("java/lang/Object"));
|
||||
assert_eq!(ir.methods.len(), 0);
|
||||
assert_eq!(ir.fields.len(), 1);
|
||||
}
|
||||
|
||||
/// Метод с ветвлениями, handler'ом, номерами строк и кадрами.
|
||||
#[test]
|
||||
fn code_with_branches_round_trip() {
|
||||
let mut pool = ConstantPool::default();
|
||||
let this = pool.add_class("demo/Flow").expect("add_class");
|
||||
let object = pool.add_class("java/lang/Object").expect("add_object");
|
||||
let name = pool.add_utf8("run").expect("name");
|
||||
let desc = pool.add_utf8("()I").expect("desc");
|
||||
let code_name = pool.add_utf8("Code").expect("attr");
|
||||
let lines_name = pool.add_utf8("LineNumberTable").expect("attr");
|
||||
let frames_name = pool.add_utf8("StackMapTable").expect("attr");
|
||||
|
||||
// Логические номера: 0 iconst_0, 1 istore_1, 2 iload_1, 3 ifeq→6,
|
||||
// 4 iconst_1, 5 goto→7, 6 iconst_2, 7 ireturn.
|
||||
let code = vec![
|
||||
Rt::Iconst_0,
|
||||
Rt::Istore_1,
|
||||
Rt::Iload_1,
|
||||
Rt::Ifeq(6),
|
||||
Rt::Iconst_1,
|
||||
Rt::Goto(7),
|
||||
Rt::Iconst_2,
|
||||
Rt::Ireturn,
|
||||
];
|
||||
let code_attr = RtAttr::Code {
|
||||
name_index: code_name,
|
||||
max_stack: 1,
|
||||
max_locals: 2,
|
||||
code,
|
||||
exception_table: vec![RtEntry {
|
||||
range_pc: 0..4,
|
||||
handler_pc: 6,
|
||||
catch_type: 0,
|
||||
}],
|
||||
attributes: vec![
|
||||
RtAttr::LineNumberTable {
|
||||
name_index: lines_name,
|
||||
line_numbers: vec![
|
||||
RtLine {
|
||||
start_pc: 0,
|
||||
line_number: 10,
|
||||
},
|
||||
RtLine {
|
||||
start_pc: 6,
|
||||
line_number: 20,
|
||||
},
|
||||
],
|
||||
},
|
||||
RtAttr::StackMapTable {
|
||||
name_index: frames_name,
|
||||
frames: vec![
|
||||
RtFrame::SameFrame { frame_type: 6 },
|
||||
RtFrame::SameFrame { frame_type: 0 },
|
||||
],
|
||||
},
|
||||
],
|
||||
};
|
||||
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![code_attr],
|
||||
}],
|
||||
attributes: vec![],
|
||||
..RtClass::default()
|
||||
};
|
||||
|
||||
let ir = round_trip(&rt);
|
||||
let method = &ir.methods[0];
|
||||
let code = method.code.as_ref().expect("есть Code");
|
||||
assert_eq!(code.max_stack, 1);
|
||||
assert_eq!(code.max_locals, 2);
|
||||
// Номера инструкций: 0,1,2,3(ifeq),4,5(goto),6,7; кадры на 6 и 7.
|
||||
let frames = &code.frames;
|
||||
assert_eq!(frames.len(), 2);
|
||||
assert!(format!("{frames:?}").contains("offset_delta: 6"));
|
||||
assert_eq!(code.exception_table.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"));
|
||||
}
|
||||
|
||||
/// `parse → write → parse` даёт тот же IR; записанное верифицируется.
|
||||
fn round_trip(rt: &RtClass<'_>) -> ClassFile {
|
||||
let mut original = Vec::new();
|
||||
rt.to_bytes(&mut original).expect("to_bytes");
|
||||
let first = format::parse(&original).expect("parse");
|
||||
let written = format::write(&first).expect("write");
|
||||
RtClass::from_bytes(&written)
|
||||
.expect("written parses")
|
||||
.verify()
|
||||
.expect("written verifies");
|
||||
let second = format::parse(&written).expect("re-parse");
|
||||
assert_eq!(first, second, "IR обязан пережить round-trip");
|
||||
second
|
||||
}
|
||||
152
crates/noxis-classfile/tests/spike_frames.rs
Normal file
152
crates/noxis-classfile/tests/spike_frames.rs
Normal file
|
|
@ -0,0 +1,152 @@
|
|||
//! 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}"
|
||||
);
|
||||
}
|
||||
110
crates/noxis-classfile/tests/spike_ristretto.rs
Normal file
110
crates/noxis-classfile/tests/spike_ristretto.rs
Normal file
|
|
@ -0,0 +1,110 @@
|
|||
//! Spike: пригоден ли `ristretto_classfile` как backend для чтения/записи.
|
||||
//!
|
||||
//! Критерии — `docs/stack.md` §2. Тест существует, чтобы ответы на вопросы
|
||||
//! spike'а не устаревали: если крейт сломает хотя бы один пункт, сборка упадёт,
|
||||
//! и решение о backend'е придётся пересмотреть.
|
||||
//!
|
||||
//! Что здесь **не** проверяется (и что выносится в задачу P1): конвертация
|
||||
//! нашего IR в их типы и обратно. Spike отвечает только на вопрос «можно ли
|
||||
//! на этом построить», а не «сколько на это уйдёт времени».
|
||||
|
||||
#![allow(clippy::expect_used, clippy::unwrap_used)]
|
||||
|
||||
use ristretto_classfile::attributes::{StackFrame, VerificationType};
|
||||
use ristretto_classfile::{ClassFile, ConstantPool, JAVA_21};
|
||||
|
||||
/// Крейт компилируется на заявленном MSRV и собирает валидный класс.
|
||||
#[test]
|
||||
fn builds_verifies_and_serializes() {
|
||||
let mut constant_pool = ConstantPool::default();
|
||||
let this_class = constant_pool.add_class("demo/Hello").expect("add_class");
|
||||
|
||||
let class_file = ClassFile {
|
||||
version: JAVA_21,
|
||||
constant_pool,
|
||||
this_class,
|
||||
..ClassFile::default()
|
||||
};
|
||||
class_file.verify().expect("verify");
|
||||
|
||||
let mut bytes = Vec::new();
|
||||
class_file.to_bytes(&mut bytes).expect("to_bytes");
|
||||
assert!(
|
||||
bytes.starts_with(&[0xCA, 0xFE, 0xBA, 0xBE]),
|
||||
"нет магии CAFEBABE"
|
||||
);
|
||||
}
|
||||
|
||||
/// Round-trip: `to_bytes` → `from_bytes` сохраняет содержимое класса.
|
||||
#[test]
|
||||
fn round_trip_preserves_identity() {
|
||||
let mut constant_pool = ConstantPool::default();
|
||||
let this_class = constant_pool.add_class("demo/Round").expect("add_class");
|
||||
let source = ClassFile {
|
||||
version: JAVA_21,
|
||||
constant_pool,
|
||||
this_class,
|
||||
..ClassFile::default()
|
||||
};
|
||||
|
||||
let mut bytes = Vec::new();
|
||||
source.to_bytes(&mut bytes).expect("to_bytes");
|
||||
let restored = ClassFile::from_bytes(&bytes).expect("from_bytes");
|
||||
|
||||
assert_eq!(
|
||||
restored.class_name().expect("class_name").to_string(),
|
||||
"demo/Round"
|
||||
);
|
||||
assert_eq!(restored.version, source.version);
|
||||
assert_eq!(restored.constant_pool.len(), source.constant_pool.len());
|
||||
}
|
||||
|
||||
/// Кадры `StackMapTable` конструируются и сериализуются публичным API —
|
||||
/// значит, мы можем **записывать** кадры, а не только читать.
|
||||
#[test]
|
||||
fn stack_map_frames_are_constructible() {
|
||||
let mut constant_pool = ConstantPool::default();
|
||||
let this_class = constant_pool.add_class("demo/Frames").expect("add_class");
|
||||
let mut class_file = ClassFile {
|
||||
version: JAVA_21,
|
||||
constant_pool,
|
||||
this_class,
|
||||
..ClassFile::default()
|
||||
};
|
||||
|
||||
// SameFrame + расширенный кадр с одним элементом на стеке.
|
||||
let frames = vec![
|
||||
StackFrame::SameFrame { frame_type: 0 },
|
||||
StackFrame::SameLocals1StackItemFrameExtended {
|
||||
frame_type: 247,
|
||||
offset_delta: 3,
|
||||
stack: vec![VerificationType::Integer],
|
||||
},
|
||||
];
|
||||
|
||||
let name_index = class_file
|
||||
.constant_pool
|
||||
.add_utf8("StackMapTable")
|
||||
.expect("add_utf8");
|
||||
class_file
|
||||
.attributes
|
||||
.push(ristretto_classfile::attributes::Attribute::StackMapTable { name_index, frames });
|
||||
|
||||
let mut bytes = Vec::new();
|
||||
class_file.to_bytes(&mut bytes).expect("to_bytes");
|
||||
let restored = ClassFile::from_bytes(&bytes).expect("from_bytes");
|
||||
|
||||
let written = restored
|
||||
.attributes
|
||||
.iter()
|
||||
.find_map(|attribute| match attribute {
|
||||
ristretto_classfile::attributes::Attribute::StackMapTable { frames, .. } => {
|
||||
Some(frames.clone())
|
||||
}
|
||||
_ => None,
|
||||
})
|
||||
.expect("атрибут StackMapTable должен пережить round-trip");
|
||||
|
||||
assert_eq!(written.len(), 2);
|
||||
assert_eq!(written[0], StackFrame::SameFrame { frame_type: 0 });
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue