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:
Loki 2026-10-08 22:01:03 +02:00
commit a641e69455
140 changed files with 17197 additions and 0 deletions

View file

@ -0,0 +1,20 @@
[package]
name = "noxis-analysis"
description = "Иерархия классов, entrypoint'ы, reflection-сайты, keep-правила"
version.workspace = true
edition.workspace = true
rust-version.workspace = true
license.workspace = true
[dependencies]
regex = { workspace = true }
noxis-classfile = { workspace = true }
noxis-core = { workspace = true }
thiserror = { workspace = true }
tracing = { workspace = true }
[lints]
workspace = true
[build-dependencies]
noxis-structure-check = { workspace = true }

View file

@ -0,0 +1,3 @@
fn main() {
noxis_structure_check::check();
}

View file

@ -0,0 +1,244 @@
//! Entrypoint'ы и точки, где имя класса/метода видно снаружи.
use std::collections::BTreeSet;
use noxis_classfile::Program;
use noxis_classfile::insn::{Const, Insn};
/// Всё, что нельзя переименовывать «как есть» и что нужно держать в keep-сете.
#[derive(Debug, Clone, Default)]
pub struct Entrypoints {
/// Классы с `public static void main(String[])`.
pub main_classes: BTreeSet<String>,
/// `native`-методы: имя связано с JNI-схемой (`Java_pkg_Class_method`).
pub native_methods: BTreeSet<(String, String, String)>,
/// Интерфейсы из `META-INF/services/*`.
pub service_interfaces: BTreeSet<String>,
/// Имена, встречающиеся в рефлексивных вызовах (`Class.forName` и т.п.).
pub reflective_names: BTreeSet<String>,
/// Имена членов (`getMethod`/`getField`-литералы): их нельзя переименовывать.
pub reflective_member_names: BTreeSet<String>,
/// Поля с флагом `transient`+`serialVersionUID`-контекст: сериализуемые классы
/// обязаны сохранить имя класса и полей (иначе ломается Java-сериализация).
pub serializable_classes: BTreeSet<String>,
}
impl Entrypoints {
/// Собрать entrypoint'ы по программе (без keep-правил — их добавляет [`Self::merge`]).
pub fn collect(program: &Program) -> Self {
let mut out = Self::default();
for class in &program.classes {
if class.this_class == "java/lang/Object" {
continue;
}
if implements_serializable(program, &class.this_class) {
out.serializable_classes.insert(class.this_class.clone());
}
for method in &class.methods {
const ACC_STATIC: u16 = 0x0008;
const ACC_NATIVE: u16 = 0x0100;
if method.access_flags & ACC_NATIVE != 0 {
out.native_methods.insert((
class.this_class.clone(),
method.name.clone(),
method.descriptor.clone(),
));
}
if method.name == "main"
&& method.descriptor == "([Ljava/lang/String;)V"
&& method.access_flags & ACC_STATIC != 0
{
out.main_classes.insert(class.this_class.clone());
}
if let Some(code) = &method.code {
collect_reflective_names(&mut out, code);
}
}
}
out.service_interfaces = collect_service_interfaces(program);
out
}
/// Объединить с другим набором (например, из keep-правил конфига).
pub fn merge(&mut self, other: &Entrypoints) {
self.main_classes.extend(other.main_classes.iter().cloned());
self.native_methods
.extend(other.native_methods.iter().cloned());
self.service_interfaces
.extend(other.service_interfaces.iter().cloned());
self.reflective_names
.extend(other.reflective_names.iter().cloned());
self.reflective_member_names
.extend(other.reflective_member_names.iter().cloned());
self.serializable_classes
.extend(other.serializable_classes.iter().cloned());
}
/// Класс, который нельзя переименовывать.
pub fn keeps_class(&self, internal_name: &str) -> bool {
self.main_classes.contains(internal_name)
|| self.serializable_classes.contains(internal_name)
|| self.reflective_names.contains(internal_name)
}
/// Член (метод/поле), который нельзя переименовывать.
pub fn keeps_member(&self, name: &str) -> bool {
self.reflective_member_names.contains(name)
}
}
/// `implements java.io.Serializable` (по цепочке предков, в пределах программы).
fn implements_serializable(program: &Program, start: &str) -> bool {
let mut stack = vec![start.to_string()];
let mut seen = std::collections::HashSet::new();
while let Some(name) = stack.pop() {
if !seen.insert(name.clone()) {
continue;
}
if name == "java/io/Serializable" {
return true;
}
let Some(class) = program.class(&name) else {
// Внешний предок: считаем, что не знаем — консервативно не keep'им.
continue;
};
if let Some(sup) = &class.super_class {
stack.push(sup.clone());
}
stack.extend(class.interfaces.iter().cloned());
}
false
}
/// `META-INF/services/<iface>` → внутреннее имя интерфейса.
fn collect_service_interfaces(program: &Program) -> BTreeSet<String> {
let mut out = BTreeSet::new();
for resource in &program.resources {
let Some(name) = resource.path.strip_prefix("META-INF/services/") else {
continue;
};
if name.is_empty() {
continue;
}
out.insert(name.replace('.', "/"));
}
out
}
/// Линейный эвристический поиск имён в рефлексивных API.
///
/// Полноценный dataflow не нужен: если рядом с вызовом `Class.forName(...)` /
/// `getMethod(...)` лежит строковый литерал — это имя видно статически,
/// значит, его нельзя переименовывать.
fn collect_reflective_names(out: &mut Entrypoints, code: &noxis_classfile::Code) {
/// API, чьи аргументы — **имена классов**.
const CLASS_API: &[(&str, &str)] = &[
("java/lang/Class", "forName"),
("java/lang/ClassLoader", "loadClass"),
];
/// API, чьи аргументы — **имена членов** (поле/метод).
const MEMBER_API: &[(&str, &str)] = &[
("java/lang/Class", "getMethod"),
("java/lang/Class", "getDeclaredMethod"),
("java/lang/Class", "getField"),
("java/lang/Class", "getDeclaredField"),
];
let mut last_string: Option<&str> = None;
for insn in &code.insns {
if let Insn::Const(Const::String(s)) = insn {
last_string = Some(s.as_str());
continue;
}
if let Insn::Invoke { owner, name, .. } = insn {
let is_class_api = CLASS_API.iter().any(|(o, n)| o == owner && n == name);
let is_member_api = MEMBER_API.iter().any(|(o, n)| o == owner && n == name);
if let (true, Some(s)) = (is_class_api, last_string) {
out.reflective_names.insert(s.replace('.', "/"));
}
if let (true, Some(s)) = (is_member_api, last_string) {
out.reflective_member_names.insert(s.to_string());
}
last_string = None;
continue;
}
// Инструкции, не относящиеся к строке/вызову, обрывают «соседность».
if !matches!(insn, Insn::Mark(_) | Insn::Nop) {
last_string = None;
}
}
}
#[cfg(test)]
mod tests {
#![allow(clippy::expect_used, clippy::unwrap_used)]
use super::*;
use noxis_classfile::insn::InvokeKind;
use noxis_classfile::{ClassBuilder, Insn, empty_code};
#[test]
fn finds_main_and_native() {
let main = ClassBuilder::new("app/Main", None)
.method_with_code(0x0009, "main", "([Ljava/lang/String;)V", empty_code(1))
.build();
let mut native_class = ClassBuilder::new("app/Jni", None).build();
native_class.methods.push(noxis_classfile::Method {
access_flags: 0x0100, // ACC_NATIVE
name: "ping".into(),
descriptor: "()I".into(),
code: None,
attributes: Vec::new(),
});
let program = Program {
classes: vec![main, native_class],
resources: Vec::new(),
};
let ep = Entrypoints::collect(&program);
assert!(ep.main_classes.contains("app/Main"));
assert!(ep.native_methods.contains(&(
"app/Jni".to_string(),
"ping".to_string(),
"()I".to_string()
)));
assert!(ep.keeps_class("app/Main"));
}
#[test]
fn collects_service_interface() {
let program = Program {
classes: Vec::new(),
resources: vec![noxis_classfile::Resource {
path: "META-INF/services/com.example.Plugin".into(),
data: b"app.Impl\n".to_vec(),
}],
};
let ep = Entrypoints::collect(&program);
assert!(ep.service_interfaces.contains("com/example/Plugin"));
}
#[test]
fn picks_reflective_literal() {
let mut code = empty_code(1);
code.insns = vec![
Insn::Const(Const::String("app.Secret".into())),
Insn::Invoke {
kind: InvokeKind::Static,
owner: "java/lang/Class".into(),
name: "forName".into(),
descriptor: "(Ljava/lang/String;)Ljava/lang/Class;".into(),
itf: false,
},
];
let class = ClassBuilder::new("app/Caller", None)
.method_with_code(0x0009, "go", "()V", code)
.build();
let program = Program {
classes: vec![class],
resources: Vec::new(),
};
let ep = Entrypoints::collect(&program);
assert!(ep.reflective_names.contains("app/Secret"));
}
}

View file

@ -0,0 +1,250 @@
//! Иерархия классов: наследование, переопределения, безопасность переименования.
use std::collections::{HashMap, HashSet};
use noxis_classfile::Program;
/// Узел иерархии: суперкласс и интерфейсы (по внутренним именам).
#[derive(Debug, Clone, Default)]
pub struct HierarchyNode {
pub super_class: Option<String>,
pub interfaces: Vec<String>,
}
/// Построенная иерархия классов.
#[derive(Debug, Clone, Default)]
pub struct ClassHierarchy {
nodes: HashMap<String, HierarchyNode>,
}
impl ClassHierarchy {
/// Построить иерархию из программы (только по имеющимся классам).
pub fn build(program: &Program) -> Self {
let mut nodes = HashMap::with_capacity(program.classes.len());
for class in &program.classes {
nodes.insert(
class.this_class.clone(),
HierarchyNode {
super_class: class.super_class.clone(),
interfaces: class.interfaces.clone(),
},
);
}
Self { nodes }
}
/// Прямой суперкласс (для LUB при выводе кадров).
pub fn superclass(&self, internal_name: &str) -> Option<String> {
self.nodes
.get(internal_name)
.and_then(|n| n.super_class.clone())
}
/// Есть ли класс в иерархии (в том числе из внешнего classpath — нет).
pub fn contains(&self, internal_name: &str) -> bool {
self.nodes.contains_key(internal_name)
}
/// Прямые предки: суперкласс + интерфейсы.
pub fn parents(&self, internal_name: &str) -> Vec<String> {
let mut out = Vec::new();
if let Some(node) = self.nodes.get(internal_name) {
out.extend(node.super_class.iter().cloned());
out.extend(node.interfaces.iter().cloned());
}
out
}
/// Является ли `child` подклассом `parent` (в пределах программы).
///
/// Неполная иерархия (внешние зависимости не грузим) — это осознанно:
/// обфускатор работает по артефакту, а не по всему classpath.
pub fn is_subclass_of(&self, child: &str, parent: &str) -> bool {
if child == parent {
return true;
}
let mut seen = HashSet::new();
let mut stack = vec![child.to_string()];
while let Some(current) = stack.pop() {
if !seen.insert(current.clone()) {
continue;
}
for p in self.parents(&current) {
if p == parent {
return true;
}
stack.push(p);
}
}
false
}
/// Все классы программы, которые наследуют `parent` (включая его самого).
pub fn descendants(&self, parent: &str) -> Vec<String> {
self.nodes
.keys()
.filter(|name| self.is_subclass_of(name, parent))
.cloned()
.collect()
}
/// Классы, объявляющие метод с данным именем/дескриптором, вверх по цепочке.
pub fn declaring_chain(
&self,
program: &Program,
start: &str,
name: &str,
descriptor: &str,
) -> Vec<String> {
let mut out = Vec::new();
let mut seen = HashSet::new();
let mut stack = vec![start.to_string()];
while let Some(current) = stack.pop() {
if !seen.insert(current.clone()) {
continue;
}
if let Some(class) = program.class(&current)
&& class.method(name, descriptor).is_some()
{
out.push(current.clone());
}
stack.extend(self.parents(&current));
}
out
}
}
impl noxis_classfile::Hierarchy for ClassHierarchy {
fn superclass(&self, internal_name: &str) -> Option<String> {
self.superclass(internal_name)
}
}
/// Почему метод нельзя/можно переименовывать (аналог `InheritanceAnalyzer`).
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum RenameSafety {
/// Переименовывать можно: это точка, где имя не видно снаружи.
Safe,
/// Конструктор/статический блок: имя зафиксировано форматом JVM.
FixedName,
/// Виртуальный метод, который может переопределяться вне артефакта.
VirtualExternal,
/// Рефлексия/сериализация может обратиться по имени.
ReferencedByName,
/// Метод не найден в классе.
Missing,
}
/// Оценка безопасности переименования метода.
///
/// Правила (сознательно консервативные, как в `OBF/InheritanceAnalyzer`):
/// - `<init>`/`<clinit>` — никогда;
/// - `private`/`static`/`final`/`<package-private>` внутри финального класса — можно;
/// - `public`/`protected`/`package-private`, если класс не `final` и есть
/// наследники (внутри артефакта или нет) — нельзя, пока нет доказательства.
pub fn method_rename_safety(
program: &Program,
hierarchy: &ClassHierarchy,
owner: &str,
name: &str,
descriptor: &str,
externally_referenced: bool,
) -> RenameSafety {
if name == "<init>" || name == "<clinit>" {
return RenameSafety::FixedName;
}
let Some(class) = program.class(owner) else {
return RenameSafety::Missing;
};
if externally_referenced {
return RenameSafety::ReferencedByName;
}
let Some(method) = class.method(name, descriptor) else {
return RenameSafety::Missing;
};
const ACC_PRIVATE: u16 = 0x0002;
const ACC_STATIC: u16 = 0x0008;
const ACC_FINAL: u16 = 0x0010;
const ACC_PUBLIC: u16 = 0x0001;
const ACC_PROTECTED: u16 = 0x0004;
let flags = method.access_flags;
if flags & ACC_PRIVATE != 0 || flags & ACC_STATIC != 0 || flags & ACC_FINAL != 0 {
return RenameSafety::Safe;
}
// Не-private без `final`: ищем переопределения в иерархии.
let chain = hierarchy.declaring_chain(program, owner, name, descriptor);
let has_subclasses = chain
.iter()
.any(|c| c != owner && hierarchy.is_subclass_of(c, owner));
if has_subclasses {
return RenameSafety::VirtualExternal;
}
if flags & (ACC_PUBLIC | ACC_PROTECTED) != 0 {
// Публичный метод финального/листьевого класса: внутри артефакта
// переопределений нет, но снаружи могут зваться по имени.
return RenameSafety::VirtualExternal;
}
RenameSafety::Safe
}
#[cfg(test)]
mod tests {
#![allow(clippy::expect_used, clippy::unwrap_used)]
use super::*;
use noxis_classfile::ClassBuilder;
fn sample_program() -> Program {
let base = ClassBuilder::new("a/Base", None)
.method_with_code(0x0001, "open", "()V", noxis_classfile::empty_code(0))
.method_with_code(0x0002, "hidden", "()V", noxis_classfile::empty_code(0))
.method_with_code(0x0001, "<init>", "()V", noxis_classfile::empty_code(0))
.build();
let derived = ClassBuilder::new("a/Der", Some("a/Base"))
.method_with_code(0x0001, "open", "()V", noxis_classfile::empty_code(0))
.build();
Program {
classes: vec![base, derived],
resources: Vec::new(),
}
}
#[test]
fn hierarchy_links_superclass() {
let program = sample_program();
let h = ClassHierarchy::build(&program);
assert!(h.is_subclass_of("a/Der", "a/Base"));
assert!(!h.is_subclass_of("a/Base", "a/Der"));
}
#[test]
fn private_method_is_safe() {
let program = sample_program();
let h = ClassHierarchy::build(&program);
assert_eq!(
method_rename_safety(&program, &h, "a/Base", "hidden", "()V", false),
RenameSafety::Safe
);
}
#[test]
fn constructor_is_never_renamed() {
let program = sample_program();
let h = ClassHierarchy::build(&program);
assert_eq!(
method_rename_safety(&program, &h, "a/Base", "<init>", "()V", false),
RenameSafety::FixedName
);
}
#[test]
fn virtual_method_with_subclass_is_unsafe() {
let program = sample_program();
let h = ClassHierarchy::build(&program);
assert_eq!(
method_rename_safety(&program, &h, "a/Base", "open", "()V", false),
RenameSafety::VirtualExternal
);
}
}

View file

@ -0,0 +1,237 @@
//! Keep-правила: что нельзя переименовывать (glob-паттерны + entrypoint'ы).
use regex::Regex;
use noxis_classfile::{Attribute, Const, Program};
use crate::entrypoints::Entrypoints;
use noxis_core::Config;
/// Скомпилированный glob: `*` — любые символы кроме `/`, `**` — всё, `?` — один.
fn compile(pattern: &str) -> Result<Regex, regex::Error> {
let mut out = String::from("^");
let chars: Vec<char> = pattern.chars().collect();
let mut i = 0;
while i < chars.len() {
match chars[i] {
'*' => {
if i + 1 < chars.len() && chars[i + 1] == '*' {
out.push_str(".*");
i += 2;
} else {
out.push_str("[^/]*");
i += 1;
}
}
'?' => {
out.push_str("[^/]");
i += 1;
}
c => {
out.push_str(&regex::escape(&c.to_string()));
i += 1;
}
}
}
out.push('$');
Regex::new(&out)
}
/// Правила «не трогать»: явные паттерны конфига + entrypoint'ы.
#[derive(Debug, Default, Clone)]
pub struct KeepRules {
class_patterns: Vec<Regex>,
resource_patterns: Vec<Regex>,
/// Точные имена, которые нельзя трогать (entrypoint'ы, рефлексия, ...).
fixed: std::collections::BTreeSet<String>,
/// Имена членов, защищённые рефлексией.
fixed_members: std::collections::BTreeSet<String>,
}
impl KeepRules {
/// Собрать правила из конфига.
pub fn from_config(config: &Config) -> Result<Self, RuleError> {
let mut class_patterns = Vec::with_capacity(config.keep.len());
for pattern in &config.keep {
class_patterns.push(compile(pattern).map_err(|e| RuleError::Pattern {
pattern: pattern.clone(),
msg: e.to_string(),
})?);
}
let mut resource_patterns = Vec::with_capacity(config.keep_resources.len());
for pattern in &config.keep_resources {
resource_patterns.push(compile(pattern).map_err(|e| RuleError::Pattern {
pattern: pattern.clone(),
msg: e.to_string(),
})?);
}
Ok(Self {
class_patterns,
resource_patterns,
fixed: std::collections::BTreeSet::new(),
fixed_members: std::collections::BTreeSet::new(),
})
}
/// Запретить переименование члена с таким именем (в любом классе).
pub fn keep_member(&mut self, name: impl Into<String>) {
self.fixed_members.insert(name.into());
}
/// Добавить точное имя класса (из анализа программы).
pub fn keep_exact(&mut self, internal_name: impl Into<String>) {
self.fixed.insert(internal_name.into());
}
/// Объединить с найденными entrypoint'ами.
pub fn absorb(&mut self, entrypoints: &Entrypoints) {
for name in &entrypoints.main_classes {
self.keep_exact(name.clone());
}
for name in &entrypoints.reflective_names {
self.keep_exact(name.clone());
}
for name in &entrypoints.serializable_classes {
self.keep_exact(name.clone());
}
for (owner, _, _) in &entrypoints.native_methods {
self.keep_exact(owner.clone());
}
for name in &entrypoints.service_interfaces {
self.keep_exact(name.clone());
}
for name in &entrypoints.reflective_member_names {
self.keep_member(name.clone());
}
}
/// Методы, на которые ссылаются `BootstrapMethods` (`invokedynamic`).
///
/// Переименование такого метода без правки bootstrap-записи ломает класс,
/// а правка — задача P2 (см. `todo.md`): пока просто не трогаем имена.
pub fn absorb_bootstrap(&mut self, program: &Program) {
for class in &program.classes {
for method in &class.methods {
for attr in &method.attributes {
if let Attribute::BootstrapMethods(methods) = attr {
for bsm in methods {
self.keep_member(bsm.name.clone());
for arg in &bsm.args {
if let Const::MethodHandle { name, .. } = arg {
self.keep_member(name.clone());
}
}
}
}
}
}
}
}
/// Нужно ли оставить класс с данным внутренним именем.
pub fn keeps_class(&self, internal_name: &str) -> bool {
self.fixed.contains(internal_name)
|| self
.class_patterns
.iter()
.any(|re| re.is_match(internal_name))
}
/// Нужно ли оставить член с таким именем (не переименовывать).
pub fn keeps_member(&self, name: &str) -> bool {
self.fixed_members.contains(name)
}
/// Нужно ли оставить ресурс с данным путём.
pub fn keeps_resource(&self, path: &str) -> bool {
self.resource_patterns.iter().any(|re| re.is_match(path))
}
/// Число правил (для отчёта).
pub fn len(&self) -> usize {
self.class_patterns.len()
+ self.resource_patterns.len()
+ self.fixed.len()
+ self.fixed_members.len()
}
/// Есть ли вообще правила.
pub fn is_empty(&self) -> bool {
self.len() == 0
}
}
/// Ошибка компиляции паттерна.
#[derive(Debug, thiserror::Error)]
pub enum RuleError {
#[error("некорректный паттерн `{pattern}`: {msg}")]
Pattern { pattern: String, msg: String },
}
/// Полный анализ перед обфускацией.
#[derive(Debug, Clone)]
pub struct Analysis {
pub entrypoints: Entrypoints,
pub keep: KeepRules,
/// Классы, которые не переименовываются.
pub locked_classes: std::collections::BTreeSet<String>,
}
impl Analysis {
/// Прогнать весь анализ по программе.
pub fn run(program: &Program, config: &Config) -> Result<Self, RuleError> {
let entrypoints = Entrypoints::collect(program);
let mut keep = KeepRules::from_config(config)?;
keep.absorb(&entrypoints);
keep.absorb_bootstrap(program);
let locked_classes = program
.classes
.iter()
.map(|c| c.this_class.clone())
.filter(|name| keep.keeps_class(name))
.collect();
Ok(Self {
entrypoints,
keep,
locked_classes,
})
}
/// Класс запрещено переименовывать.
pub fn is_locked(&self, internal_name: &str) -> bool {
self.locked_classes.contains(internal_name)
}
}
#[cfg(test)]
mod tests {
#![allow(clippy::expect_used, clippy::unwrap_used)]
use super::*;
#[test]
fn glob_semantics() {
let rules = KeepRules {
class_patterns: vec![
compile("com/example/*").unwrap(),
compile("**/internal/**").unwrap(),
],
resource_patterns: vec![compile("META-INF/services/*").unwrap()],
fixed: std::collections::BTreeSet::new(),
fixed_members: std::collections::BTreeSet::new(),
};
assert!(rules.keeps_class("com/example/Main"));
assert!(!rules.keeps_class("com/example/sub/Main"));
assert!(rules.keeps_class("x/internal/y/Z"));
assert!(rules.keeps_resource("META-INF/services/a.b.C"));
assert!(!rules.keeps_resource("assets/mc/mod.json"));
}
#[test]
fn exact_names_win() {
let mut rules = KeepRules::default();
rules.keep_exact("app/Main");
assert!(rules.keeps_class("app/Main"));
assert!(!rules.keeps_class("app/Other"));
}
}

View file

@ -0,0 +1,17 @@
//! # noxis-analysis
//!
//! Анализ программы перед обфускацией: иерархия наследования, entrypoint'ы,
//! рефлексивные обращения по имени, keep-правила.
//!
//! Из этих данных пасс переименования узнаёт, **что** трогать нельзя.
//! Урок из `info/evo-weaknesses.md`: у EvoVis рефлексия по имени была
//! открытым текстом, а entrypoint'ы — в `fabric.mod.json`; здесь они
//! обязаны попадать в keep-набор автоматически.
pub mod entrypoints;
pub mod hierarchy;
pub mod keep;
pub use entrypoints::Entrypoints;
pub use hierarchy::{ClassHierarchy, RenameSafety, method_rename_safety};
pub use keep::{Analysis, KeepRules, RuleError};

View file

@ -0,0 +1,18 @@
[package]
name = "noxis-classfile"
description = "Модель классовых файлов JVM и собственный IR обфускатора"
version.workspace = true
edition.workspace = true
rust-version.workspace = true
license.workspace = true
[dependencies]
indexmap = { workspace = true }
thiserror = { workspace = true }
ristretto_classfile = "0.34"
[lints]
workspace = true
[build-dependencies]
noxis-structure-check = { workspace = true }

View file

@ -0,0 +1,3 @@
fn main() {
noxis_structure_check::check();
}

View file

@ -0,0 +1,17 @@
//! Ошибки разбора/записи классовых файлов.
/// Ошибка на уровне одного классового файла.
#[derive(Debug, thiserror::Error)]
pub enum ClassfileError {
#[error("не удалось разобрать классовый файл: {0}")]
Parse(String),
#[error("не удалось записать классовый файл: {0}")]
Write(String),
#[error("некорректный байткод {class}: {msg}")]
InvalidCode { class: String, msg: String },
#[error("не реализовано: {0}")]
Unimplemented(&'static str),
}

View file

@ -0,0 +1,121 @@
//! Эмиссия компактных кадров в точках, где их ждёт верификатор.
//!
//! Вид кадра выбирается сравнением с предыдущим кадром (`Same`/`Append`/
//! `Chop`/`SameLocals`, иначе `Full`) — как у `javac`. Хвостовые `Top`
//! при сравнении и записи отрезаются: пустой слот — не тип.
use std::collections::BTreeSet;
use super::merge::Merger;
use super::xfer::State;
use super::{StackMapFrame, VerificationType};
use crate::error::ClassfileError;
/// Кадры во всех точках `targets` (отсортированы — дельты по порядку).
///
/// Дельты — в номерах инструкций (как backend после чтения); writer сам
/// пересчитает их в байты.
pub(super) fn emit_frames(
states: &[Option<State>],
targets: &BTreeSet<u32>,
initial: &[VerificationType],
merger: &Merger<'_>,
) -> Result<Vec<StackMapFrame>, ClassfileError> {
let mut out = Vec::new();
let mut previous_locals = initial.to_vec();
let mut previous_index: Option<u32> = None;
for target in targets {
let at = *target as usize;
let Some(state) = &states[at] else {
continue;
};
let delta = match previous_index {
None => *target,
Some(previous) => target
.checked_sub(previous)
.and_then(|delta| delta.checked_sub(1))
.ok_or_else(|| merger.invalid("кадры не по порядку"))?,
};
out.push(select(delta, &state.locals, &previous_locals, &state.stack));
previous_locals = state.locals.clone();
previous_index = Some(*target);
}
Ok(out)
}
fn select(
delta: u32,
locals: &[VerificationType],
previous: &[VerificationType],
stack: &[VerificationType],
) -> StackMapFrame {
// Сравнение — на гранулярности переменных (`long` — одна запись,
// как у `javac`): иначе половины пар дают `Append{[Top, Long]}`.
let current = vars(locals);
let base = vars(previous);
if current == base {
if stack.is_empty() {
return StackMapFrame::Same {
offset_delta: delta,
};
}
if let [local] = stack {
return StackMapFrame::SameLocals {
offset_delta: delta,
local: local.clone(),
};
}
}
if stack.is_empty()
&& let Some(added) = current.strip_prefix(base.as_slice())
&& (1..=3).contains(&added.len())
&& !added.contains(&VerificationType::Top)
{
return StackMapFrame::Append {
offset_delta: delta,
locals: added.to_vec(),
};
}
if stack.is_empty()
&& let Some(dropped) = base.strip_prefix(current.as_slice())
&& (1..=3).contains(&dropped.len())
&& let Ok(count) = u8::try_from(dropped.len())
{
return StackMapFrame::Chop {
offset_delta: delta,
count,
};
}
StackMapFrame::Full {
offset_delta: delta,
locals: current,
stack: stack.to_vec(),
}
}
/// Слоты → переменные: `[Long, Top]` — одна запись; хвостовые `Top` — прочь.
///
/// Одиночные `Top` внутри (дыры) сохраняются: в `Full` это легальный тип,
/// а в `Append` мы их не пускаем (см. `select`).
fn vars(locals: &[VerificationType]) -> Vec<VerificationType> {
let mut out = Vec::with_capacity(locals.len());
let mut index = 0;
while index < locals.len() {
match &locals[index] {
VerificationType::Long | VerificationType::Double
if locals.get(index + 1) == Some(&VerificationType::Top) =>
{
out.push(locals[index].clone());
index += 2;
}
other => {
out.push(other.clone());
index += 1;
}
}
}
while out.last() == Some(&VerificationType::Top) {
out.pop();
}
out
}

View file

@ -0,0 +1,122 @@
//! CFG тела метода: метки, рёбра, точки кадров, диапазоны handler'ов.
use std::collections::{BTreeMap, BTreeSet};
use super::merge::Merger;
use crate::error::ClassfileError;
use crate::insn::{Insn, Label};
use crate::model::ExceptionHandler;
/// Метки → номера инструкций без маркеров.
pub(super) fn strip_marks<'a>(
insns: &'a [Insn],
merger: &Merger<'_>,
) -> Result<(BTreeMap<Label, u32>, Vec<&'a Insn>), ClassfileError> {
let mut labels = BTreeMap::new();
let mut bare = Vec::with_capacity(insns.len());
for insn in insns {
match insn {
Insn::Mark(label) => {
let index = u32::try_from(bare.len())
.map_err(|_| merger.invalid("слишком много инструкций"))?;
if let Some(prev) = labels.insert(*label, index)
&& prev != index
{
return Err(merger.invalid(&format!("метка {label} объявлена дважды")));
}
}
_ => bare.push(insn),
}
}
Ok((labels, bare))
}
/// Рёбра CFG без исключений: продолжение + цели переходов.
pub(super) fn successors(
insn: &Insn,
index: u32,
len: u32,
labels: &BTreeMap<Label, u32>,
merger: &Merger<'_>,
) -> Result<Vec<u32>, ClassfileError> {
let mut out = Vec::new();
// Без продолжения: переходы без возврата и выходы из метода.
// `Jsr`/`Ret` сюда не доходят (их отвергает `transfer`), но рёбра есть.
let terminal = matches!(
insn,
Insn::Goto(..)
| Insn::Jsr(..)
| Insn::Ret(..)
| Insn::TableSwitch { .. }
| Insn::LookupSwitch { .. }
| Insn::Return(..)
| Insn::AReturn
| Insn::AThrow
);
if !terminal && index + 1 < len {
out.push(index + 1);
}
for target in insn.targets() {
out.push(
labels
.get(&target)
.copied()
.ok_or_else(|| merger.invalid(&format!("нет метки {target}")))?,
);
}
Ok(out)
}
/// Точки, где верификатор требует кадры: цели переходов + входы handler'ов.
pub(super) fn frame_targets(
bare: &[&Insn],
labels: &BTreeMap<Label, u32>,
handlers: &[HandlerRange],
merger: &Merger<'_>,
) -> Result<BTreeSet<u32>, ClassfileError> {
let mut targets = BTreeSet::new();
for insn in bare {
for target in insn.targets() {
targets.insert(
labels
.get(&target)
.copied()
.ok_or_else(|| merger.invalid(&format!("нет метки {target}")))?,
);
}
}
for (_, _, handler, _) in handlers {
targets.insert(*handler);
}
Ok(targets)
}
/// Точка handler'а в номерах инструкций: `(start, end, handler, тип перехвата)`.
///
/// Диапазоны полуоткрытые `[start, end)`, как в JVMS.
pub(super) type HandlerRange = (u32, u32, u32, Option<String>);
/// Handler'ы в номерах инструкций для рёбер исключений.
pub(super) fn handler_ranges(
table: &[ExceptionHandler],
labels: &BTreeMap<Label, u32>,
merger: &Merger<'_>,
) -> Result<Vec<HandlerRange>, ClassfileError> {
table
.iter()
.map(|handler| {
let index_of = |label: Label| -> Result<u32, ClassfileError> {
labels
.get(&label)
.copied()
.ok_or_else(|| merger.invalid(&format!("нет метки {label}")))
};
Ok((
index_of(handler.start)?,
index_of(handler.end)?,
index_of(handler.handler)?,
handler.catch_type.clone(),
))
})
.collect()
}

View file

@ -0,0 +1,158 @@
//! Разбор дескрипторов типов (`(...)V`, `[L...;`).
use super::super::VerificationType;
/// Ошибка разбора дескриптора (контекст добавляет вызывающий код).
#[derive(Debug)]
pub(crate) struct DescError(pub String);
/// Уложить слоты значения в locals с позиции; вернуть следующую позицию.
pub(crate) fn write_local(
locals: &mut [VerificationType],
slot: usize,
value: Vec<VerificationType>,
) -> Result<usize, ()> {
if slot + value.len() > locals.len() {
return Err(());
}
let width = value.len().max(1);
for (offset, item) in value.into_iter().enumerate() {
locals[slot + offset] = item;
}
Ok(slot + width)
}
/// Разобрать дескриптор массива на (глубина, элемент без `;`).
pub(crate) fn split_array(descriptor: &str) -> Option<(usize, &str)> {
let dims = descriptor.bytes().take_while(|&b| b == b'[').count();
if dims == 0 {
return None;
}
Some((dims, descriptor[dims..].trim_end_matches(';')))
}
/// Слоты одного полевого дескриптора (`long`/`double` — парой с `Top`).
pub(crate) fn field_slots(descriptor: &str) -> Result<Vec<VerificationType>, DescError> {
let mut chars = descriptor.chars();
match chars.next() {
Some('B') | Some('C') | Some('I') | Some('S') | Some('Z') => {
Ok(vec![VerificationType::Integer])
}
Some('F') => Ok(vec![VerificationType::Float]),
Some('J') => Ok(vec![VerificationType::Long, VerificationType::Top]),
Some('D') => Ok(vec![VerificationType::Double, VerificationType::Top]),
Some('V') => Ok(Vec::new()),
Some('L') => {
let mut name = String::new();
for char in chars {
if char == ';' {
return Ok(vec![VerificationType::Object(name)]);
}
name.push(char);
}
Err(DescError(format!("нет `;` в {descriptor}")))
}
Some('[') => Ok(vec![VerificationType::Object(descriptor.to_string())]),
_ => Err(DescError(format!("кривой дескриптор {descriptor}"))),
}
}
/// Слоты параметров и возврата (`None` — `void`).
type MethodSlots = (Vec<Vec<VerificationType>>, Option<Vec<VerificationType>>);
/// Дескриптор метода: слоты параметров и слоты возврата (`None` — `void`).
pub(crate) fn method_slots(descriptor: &str) -> Result<MethodSlots, DescError> {
let (params, ret) = descriptor
.split_once('(')
.and_then(|(_, rest)| rest.split_once(')'))
.ok_or_else(|| DescError(format!("кривой дескриптор {descriptor}")))?;
let mut out = Vec::new();
let mut index = 0;
let bytes = params.as_bytes();
while index < bytes.len() {
match bytes[index] as char {
'B' | 'C' | 'I' | 'S' | 'Z' => {
out.push(vec![VerificationType::Integer]);
index += 1;
}
'F' => {
out.push(vec![VerificationType::Float]);
index += 1;
}
'J' => {
out.push(vec![VerificationType::Long, VerificationType::Top]);
index += 1;
}
'D' => {
out.push(vec![VerificationType::Double, VerificationType::Top]);
index += 1;
}
'L' => {
let end = params[index..]
.find(';')
.ok_or_else(|| DescError(format!("нет `;` в {descriptor}")))?;
out.push(vec![VerificationType::Object(
params[index + 1..index + end].to_string(),
)]);
index += end + 1;
}
'[' => {
let mut end = index;
while bytes[end] == b'[' {
end += 1;
}
if bytes[end] == b'L' {
let semi = params[end..]
.find(';')
.ok_or_else(|| DescError(format!("нет `;` в {descriptor}")))?;
end += semi + 1;
} else {
end += 1;
}
if end > bytes.len() {
return Err(DescError(format!("кривой дескриптор {descriptor}")));
}
out.push(vec![VerificationType::Object(
params[index..end].to_string(),
)]);
index = end;
}
_ => return Err(DescError(format!("кривой дескриптор {descriptor}"))),
}
}
let ret = if ret == "V" {
None
} else {
Some(field_slots(ret)?)
};
Ok((out, ret))
}
/// Тип элемента по дескриптору массива в стеке.
pub(crate) fn array_element(
array: &VerificationType,
kind: crate::insn::ArrayKind,
) -> VerificationType {
use crate::insn::ArrayKind::{Boolean, Byte, Char, Double, Float, Int, Long, Reference, Short};
if let VerificationType::Object(descriptor) = array
&& let Some((dims, elem)) = split_array(descriptor)
{
if dims > 1 {
return VerificationType::Object(descriptor[1..].to_string());
}
return match elem {
"Z" | "B" | "C" | "S" | "I" => VerificationType::Integer,
"J" => VerificationType::Long,
"F" => VerificationType::Float,
"D" => VerificationType::Double,
_ => VerificationType::Object(format!("[{elem};")),
};
}
match kind {
Boolean | Byte | Short | Char | Int => VerificationType::Integer,
Long => VerificationType::Long,
Float => VerificationType::Float,
Double => VerificationType::Double,
Reference => VerificationType::Object("java/lang/Object".to_string()),
}
}

View file

@ -0,0 +1,198 @@
//! Слияние типов (LUB) и разбор дескрипторов.
//!
//! Решётка: `Top` — «пусто» (берётся сосед), `Null` впитывается в ссылку,
//! одинаковые — сами в себя, ссылки — наименьший общий предок по иерархии.
//! Несочетаемые инициализированные типы (`int` vs ссылка после `astore`
//! в handler'е) дают `Top`, как обрезанные кадры `javac`: `Top` в кадре
//! принимает любое фактическое значение, поэтому это всегда sound.
//! Исключение — неинициализированные типы: их слияние означает сломанный
//! поток конструирования, здесь честная ошибка, а не молчаливый `Top`.
pub(super) mod descs;
use super::VerificationType;
use super::xfer::State;
use crate::error::ClassfileError;
use crate::model::Method;
use descs::write_local;
/// Иерархия классов для LUB: достаточно цепочки суперклассов.
pub trait Hierarchy {
/// Прямой суперкласс (`None` — `java/lang/Object` или неизвестный).
fn superclass(&self, class: &str) -> Option<String>;
}
/// Контекст слияния: имена для ошибок + иерархия для LCA.
pub(super) struct Merger<'h> {
pub class: String,
pub method: String,
pub hierarchy: &'h dyn Hierarchy,
}
impl Merger<'_> {
pub fn invalid(&self, msg: &str) -> ClassfileError {
ClassfileError::InvalidCode {
class: self.class.clone(),
msg: format!("{}: {msg}", self.method),
}
}
/// Влить входящее состояние; `true` — слот изменился.
pub fn merge_into(
&self,
slot: &mut Option<State>,
incoming: State,
) -> Result<bool, ClassfileError> {
match slot {
None => {
*slot = Some(incoming);
Ok(true)
}
Some(old) => {
let merged = self.lub_states(old, &incoming)?;
if *old == merged {
Ok(false)
} else {
*old = merged;
Ok(true)
}
}
}
}
/// Начальное состояние: параметры (+ `this`) поверх `Top`.
pub fn initial(&self, method: &Method, max_locals: u16) -> Result<State, ClassfileError> {
let is_static = method.access_flags & 0x0008 != 0;
let (params, _) =
descs::method_slots(&method.descriptor).map_err(|e| self.invalid(&e.0))?;
let mut locals = vec![VerificationType::Top; max_locals as usize];
let mut slot = 0;
if !is_static {
let this = if method.name == "<init>" {
VerificationType::UninitializedThis
} else {
VerificationType::Object(self.class.clone())
};
slot = write_local(&mut locals, slot, vec![this])
.map_err(|_| self.invalid("параметры не влезают в locals"))?;
}
for param in params {
slot = write_local(&mut locals, slot, param)
.map_err(|_| self.invalid("параметры не влезают в locals"))?;
}
Ok(State {
locals,
stack: Vec::new(),
})
}
fn lub_states(&self, first: &State, second: &State) -> Result<State, ClassfileError> {
if first.locals.len() != second.locals.len() {
return Err(self.invalid("разная длина locals при слиянии"));
}
if first.stack.len() != second.stack.len() {
return Err(self.invalid("разная глубина стека при слиянии"));
}
Ok(State {
locals: first
.locals
.iter()
.zip(&second.locals)
.map(|(a, b)| self.lub_types(a, b))
.collect::<Result<_, _>>()?,
stack: first
.stack
.iter()
.zip(&second.stack)
.map(|(a, b)| self.lub_types(a, b))
.collect::<Result<_, _>>()?,
})
}
fn lub_types(
&self,
first: &VerificationType,
second: &VerificationType,
) -> Result<VerificationType, ClassfileError> {
use VerificationType::{Double, Float, Integer, Long, Null, Object, Top};
if first == second {
return Ok(first.clone());
}
match (first, second) {
(Top, other) | (other, Top) => Ok(other.clone()),
(Null, other @ Object(_)) | (other @ Object(_), Null) => Ok(other.clone()),
(Null, Null) => Ok(Null),
(Integer, Integer) | (Float, Float) | (Long, Long) | (Double, Double) => {
Ok(first.clone())
}
(Object(a), Object(b)) => Ok(Object(self.common_superclass(a, b))),
// Одинаковые уже разобраны выше; здесь неинициализированные
// всегда разные — это сломанный поток конструирования.
(VerificationType::UninitializedThis | VerificationType::Uninitialized(_), _)
| (_, VerificationType::UninitializedThis | VerificationType::Uninitialized(_)) => {
Err(self.invalid("слияние неинициализированных типов"))
}
// Несочетаемые инициализированные (например, `int` из тела `try`
// и `Throwable` из handler'а в одном слоте): `Top`, как у `javac`.
_ => Ok(Top),
}
}
/// Наименьший общий предок: подъём по цепочкам, запасной — `Object`.
fn common_superclass(&self, first: &str, second: &str) -> String {
if first == second {
return first.to_string();
}
if descs::split_array(first).is_some() || descs::split_array(second).is_some() {
return self.array_lca(first, second);
}
let mut chain = Vec::new();
let mut current = Some(first.to_string());
let mut guard = 0;
while let Some(name) = current {
if name == second {
return name;
}
chain.push(name.clone());
current = self.hierarchy.superclass(&name);
guard += 1;
if guard > 1024 {
break;
}
}
let mut current = Some(second.to_string());
guard = 0;
while let Some(name) = current {
if chain.contains(&name) {
return name;
}
current = self.hierarchy.superclass(&name);
guard += 1;
if guard > 1024 {
break;
}
}
"java/lang/Object".to_string()
}
/// LCA массивов: та же размерность + LCA элементов, иначе `Object`.
fn array_lca(&self, first: &str, second: &str) -> String {
let (dims_a, elem_a) = descs::split_array(first).unwrap_or((0, first));
let (dims_b, elem_b) = descs::split_array(second).unwrap_or((0, second));
if dims_a == 0 || dims_b == 0 || dims_a != dims_b {
return "java/lang/Object".to_string();
}
if elem_a == elem_b {
return first.to_string();
}
if elem_a.len() == 1 || elem_b.len() == 1 {
return "java/lang/Object".to_string();
}
let common = self.common_superclass(
elem_a.strip_prefix('L').unwrap_or(elem_a),
elem_b.strip_prefix('L').unwrap_or(elem_b),
);
format!("{}L{common};", "[".repeat(dims_a))
}
}

View file

@ -0,0 +1,235 @@
//! `StackMapTable`: представление фреймов и вычисление после трансформаций.
//!
//! Фреймы — самая дорогая часть любого обфускатора на Rust: JVM-верификатор
//! (class version ≥ 50) требует точного `StackMapTable`, иначе класс не грузится.
//! Варианты реализации и их цены — `docs/stack.md`.
//!
//! Метод: CFG по меткам → worklist с типами (`xfer`) → слияние LUB (`merge`)
//! → эмиссия компактных кадров (`emit`). Байтовые смещения берутся тем же
//! преобразованием, что использует writer, — иначе кадры разъедутся с байтами.
mod emit;
mod flow;
mod merge;
mod xfer;
#[cfg(test)]
mod tests;
use std::collections::{BTreeMap, VecDeque};
use crate::error::ClassfileError;
use crate::insn::{Insn, Label, Prim};
use crate::model::Method;
use flow::{HandlerRange, frame_targets, handler_ranges, strip_marks, successors};
pub use merge::Hierarchy;
use merge::Merger;
use xfer::{State, transfer};
/// Тип верификации из JVMS §4.10.1.
#[derive(Debug, Clone, PartialEq)]
pub enum VerificationType {
Top,
Integer,
Float,
Double,
Long,
Null,
UninitializedThis,
/// Внутреннее имя класса (`java/lang/Object`) или дескриптор массива.
Object(String),
/// Байтовое смещение инструкции `new`.
Uninitialized(u16),
}
impl VerificationType {
/// Тип, соответствующий примитиву.
pub fn of_prim(p: Prim) -> Self {
match p {
Prim::Int => VerificationType::Integer,
Prim::Float => VerificationType::Float,
Prim::Long => VerificationType::Long,
Prim::Double => VerificationType::Double,
}
}
}
/// Запись `StackMapTable` (JVMS §4.7.4).
#[derive(Debug, Clone, PartialEq)]
pub enum StackMapFrame {
Same {
offset_delta: u32,
},
SameLocals {
offset_delta: u32,
local: VerificationType,
},
Chop {
offset_delta: u32,
count: u8,
},
Append {
offset_delta: u32,
locals: Vec<VerificationType>,
},
Full {
offset_delta: u32,
locals: Vec<VerificationType>,
stack: Vec<VerificationType>,
},
}
/// Вычислить фреймы для тела метода после всех трансформаций.
///
/// Кадры нужны только классам версии ≥ 50; `jsr`/`ret` отвергаются (такие
/// классы старше и кадры им не нужны). Иерархия — для LUB ссылочных типов.
pub fn compute_frames(
class: &str,
method: &Method,
hierarchy: &dyn Hierarchy,
) -> Result<Vec<StackMapFrame>, ClassfileError> {
let code = method
.code
.as_ref()
.ok_or_else(|| invalid(class, &method.name, "нет тела"))?;
if code.insns.is_empty() {
return Ok(Vec::new());
}
let merger = Merger {
class: class.to_string(),
method: method.name.clone(),
hierarchy,
};
let (labels, bare) = strip_marks(&code.insns, &merger)?;
let bytes = layout(&bare, &labels, &merger)?;
let handlers = handler_ranges(&code.exception_table, &labels, &merger)?;
let initial = merger.initial(method, code.max_locals)?;
let mut states: Vec<Option<State>> = vec![None; bare.len()];
states[0] = Some(initial.clone());
run_worklist(&merger, &bare, &bytes, &labels, &handlers, &mut states)?;
let targets = frame_targets(&bare, &labels, &handlers, &merger)?;
emit::emit_frames(&states, &targets, &initial.locals, &merger)
}
/// Байтовое смещение каждой инструкции — тем же кодированием, что в writer'е.
fn layout(
bare: &[&Insn],
labels: &BTreeMap<Label, u32>,
merger: &Merger<'_>,
) -> Result<Vec<u16>, ClassfileError> {
use crate::format::write::{code, pool::Pool};
let mut pool = Pool::new();
let resolve = |label: Label| -> Result<u32, ClassfileError> {
labels
.get(&label)
.copied()
.ok_or_else(|| merger.invalid(&format!("нет метки {label}")))
};
let mut rt_insns = Vec::with_capacity(bare.len());
for (index, insn) in bare.iter().enumerate() {
let index = u32::try_from(index).map_err(|_| merger.invalid("слишком много инструкций"))?;
if let Some(rt) = code::insns::convert(insn, index, &mut pool, &resolve)? {
rt_insns.push(rt);
}
}
let map = code::measure(&rt_insns)?;
(0..bare.len())
.map(|index| {
map.get(&(index as u32))
.copied()
.ok_or_else(|| merger.invalid("замер потерял инструкцию"))
})
.collect()
}
/// Основной цикл: состояния текут по рёбрам CFG до неподвижной точки.
fn run_worklist(
merger: &Merger<'_>,
bare: &[&Insn],
bytes: &[u16],
labels: &BTreeMap<Label, u32>,
handlers: &[HandlerRange],
states: &mut [Option<State>],
) -> Result<(), ClassfileError> {
let mut queue: VecDeque<u32> = VecDeque::from([0]);
let mut fuel = 1_000_000u32;
while let Some(index) = queue.pop_front() {
let at = index as usize;
let state = states[at]
.clone()
.ok_or_else(|| merger.invalid("пустое состояние"))?;
let mut out = state;
transfer(&merger.class, &merger.method, bare[at], bytes[at], &mut out).map_err(
|error| match error {
ClassfileError::InvalidCode { class, msg } => ClassfileError::InvalidCode {
class,
msg: format!("[{at}] {msg}"),
},
other => other,
},
)?;
let nexts = successors(bare[at], index, bare.len() as u32, labels, merger)?;
for next in nexts {
flow_into(merger, states, &mut queue, &mut fuel, next, out.clone())?;
}
for (start, end, handler, catch) in handlers {
if index >= *start && index < *end {
let caught = State {
locals: out.locals.clone(),
stack: vec![catch_type(catch)],
};
flow_into(merger, states, &mut queue, &mut fuel, *handler, caught)?;
}
}
}
Ok(())
}
/// Влить состояние в инструкцию; `true` — изменилось, продолжить обход.
fn flow_into(
merger: &Merger<'_>,
states: &mut [Option<State>],
queue: &mut VecDeque<u32>,
fuel: &mut u32,
next: u32,
state: State,
) -> Result<(), ClassfileError> {
if *fuel == 0 {
return Err(merger.invalid("нет неподвижной точки"));
}
*fuel -= 1;
let slot = states
.get_mut(next as usize)
.ok_or_else(|| merger.invalid("переход вне тела метода"))?;
let changed = merger
.merge_into(slot, state)
.map_err(|error| match error {
ClassfileError::InvalidCode { class, msg } => ClassfileError::InvalidCode {
class,
msg: format!("[->{next}] {msg}"),
},
other => other,
})?;
if changed {
queue.push_back(next);
}
Ok(())
}
/// Рёбра CFG без исключений: продолжение + цели переходов.
/// Тип исключения на входе в handler (`catch-all` — любое throwable).
fn catch_type(catch: &Option<String>) -> VerificationType {
match catch {
Some(name) => VerificationType::Object(name.clone()),
None => VerificationType::Object("java/lang/Throwable".to_string()),
}
}
fn invalid(class: &str, method: &str, msg: &str) -> ClassfileError {
ClassfileError::InvalidCode {
class: class.to_string(),
msg: format!("{method}: {msg}"),
}
}

View file

@ -0,0 +1,179 @@
//! Unit-тесты `compute_frames` на синтезированном IR.
#![allow(clippy::expect_used, clippy::unwrap_used)]
use super::{StackMapFrame, VerificationType, compute_frames};
use crate::Hierarchy;
use crate::insn::{BranchCond, Cond, Const, Insn, Prim, SlotKind};
use crate::model::{Code, ExceptionHandler, Method};
/// Пустая иерархия: всё неизвестное — наследники `Object` (для LUB хватает).
struct NoHierarchy;
impl Hierarchy for NoHierarchy {
fn superclass(&self, _class: &str) -> Option<String> {
None
}
}
fn code_method(
name: &str,
descriptor: &str,
max_stack: u16,
max_locals: u16,
insns: Vec<Insn>,
table: Vec<ExceptionHandler>,
) -> Method {
Method {
access_flags: 0x0008,
name: name.to_string(),
descriptor: descriptor.to_string(),
code: Some(Code {
max_stack,
max_locals,
insns,
exception_table: table,
frames: vec![],
attributes: vec![],
}),
attributes: vec![],
}
}
/// Ветвь → два кадра (`Same`, `SameLocals`).
#[test]
fn branch_emits_same_frames() {
let mut method = code_method(
"max",
"(II)I",
2,
2,
vec![
Insn::Mark(0),
load(0),
load(1),
Insn::Branch(BranchCond::Int(Cond::Ge), 1),
Insn::Const(Const::Int(0)),
Insn::Goto(2),
Insn::Mark(1),
Insn::Const(Const::Int(1)),
Insn::Mark(2),
Insn::Return(Some(Prim::Int)),
],
vec![],
);
method.access_flags = 0x0009;
let frames = compute_frames("demo/Max", &method, &NoHierarchy).expect("frames");
assert_eq!(
frames,
vec![
StackMapFrame::Same { offset_delta: 5 },
StackMapFrame::SameLocals {
offset_delta: 0,
local: VerificationType::Integer,
},
]
);
}
/// `try/catch`: handler получает `Full`, продолжение — `Append`.
#[test]
fn handler_emits_full_frame() {
let method = code_method(
"run",
"()V",
1,
2,
vec![
Insn::Mark(0),
Insn::Const(Const::Int(0)),
store(0),
Insn::Mark(1),
Insn::Const(Const::Int(1)),
store(0),
Insn::Goto(3),
Insn::Mark(2),
store_ref(1),
Insn::Const(Const::Int(2)),
store(0),
Insn::Mark(3),
Insn::Return(None),
],
vec![ExceptionHandler {
start: 1,
end: 2,
handler: 2,
catch_type: Some("java/lang/Exception".to_string()),
}],
);
let frames = compute_frames("demo/Try", &method, &NoHierarchy).expect("frames");
assert_eq!(
frames,
vec![
StackMapFrame::Full {
offset_delta: 5,
locals: vec![VerificationType::Integer],
stack: vec![VerificationType::Object("java/lang/Exception".to_string())],
},
StackMapFrame::Append {
offset_delta: 2,
locals: vec![VerificationType::Object("java/lang/Exception".to_string())],
},
]
);
}
/// Два `new` на разных ветвях нельзя слить — честная ошибка, не `Top`.
#[test]
fn uninitialized_merge_is_rejected() {
let method = code_method(
"pick",
"(Z)Ljava/lang/Object;",
1,
2,
vec![
Insn::Mark(0),
load(0),
Insn::Branch(BranchCond::Zero(Cond::Ne), 1),
Insn::New("a/A".to_string()),
store_ref(1),
Insn::Goto(2),
Insn::Mark(1),
Insn::New("a/A".to_string()),
store_ref(1),
Insn::Mark(2),
Insn::Load {
slot: 1,
kind: SlotKind::Ref,
},
Insn::AReturn,
],
vec![],
);
let err = compute_frames("demo/Pick", &method, &NoHierarchy).expect_err("слияние");
assert!(
err.to_string().contains("неинициализированных"),
"не тот текст: {err}"
);
}
fn load(slot: u16) -> Insn {
Insn::Load {
slot,
kind: SlotKind::Int,
}
}
fn store(slot: u16) -> Insn {
Insn::Store {
slot,
kind: SlotKind::Int,
}
}
fn store_ref(slot: u16) -> Insn {
Insn::Store {
slot,
kind: SlotKind::Ref,
}
}

View file

@ -0,0 +1,133 @@
//! Вызовы и константы: дескрипторы, `<init>`, слоты значений.
use super::super::VerificationType;
use super::super::merge::descs::{DescError, field_slots, method_slots};
use super::State;
use crate::error::ClassfileError;
use crate::insn::{Const, Convert, InvokeKind, Prim};
pub(super) fn const_slots(value: &Const) -> Result<Vec<VerificationType>, DescError> {
Ok(match value {
Const::Int(_) => vec![VerificationType::Integer],
Const::Long(_) => vec![VerificationType::Long, VerificationType::Top],
Const::Float(_) => vec![VerificationType::Float],
Const::Double(_) => vec![VerificationType::Double, VerificationType::Top],
Const::String(_) => vec![VerificationType::Object("java/lang/String".to_string())],
Const::Class(_) => vec![VerificationType::Object("java/lang/Class".to_string())],
Const::MethodType(_) => vec![VerificationType::Object(
"java/lang/invoke/MethodType".to_string(),
)],
Const::MethodHandle { .. } => vec![VerificationType::Object(
"java/lang/invoke/MethodHandle".to_string(),
)],
Const::Dynamic { descriptor, .. } => field_slots(descriptor)?,
})
}
pub(super) fn invoke(
class: &str,
method: &str,
state: &mut State,
kind: InvokeKind,
owner: &str,
name: &str,
descriptor: &str,
) -> Result<(), ClassfileError> {
let (params, ret) = method_slots(descriptor).map_err(|e| context(class, method, e))?;
super::stack::pop_slots(
state,
count_slots(&params),
class,
method,
"аргументы вызова",
)?;
let mut receiver = None;
if kind != InvokeKind::Static {
receiver = super::stack::pop_slots(state, 1, class, method, "objectref")?.pop();
}
// `<init>` инициализирует получателя во ВСЕХ его копиях.
if name == "<init>" {
match receiver {
Some(VerificationType::UninitializedThis) => {
replace(
state,
&VerificationType::UninitializedThis,
&initialized(class),
);
}
Some(VerificationType::Uninitialized(_)) => {
let fresh = VerificationType::Object(owner.to_string());
replace_uninit(state, &fresh);
}
_ => {}
}
}
if let Some(ret) = ret {
super::stack::push(state, ret)?;
}
Ok(())
}
/// Число слотов всех параметров.
pub(super) fn count_slots(params: &[Vec<VerificationType>]) -> usize {
params.iter().map(Vec::len).sum()
}
/// Заменить все вхождения типа новым (инициализация через `<init>`).
fn replace(state: &mut State, from: &VerificationType, to: &VerificationType) {
for slot in state.locals.iter_mut().chain(state.stack.iter_mut()) {
if slot == from {
*slot = to.clone();
}
}
}
/// Любой `Uninitialized` (кроме `this`) → тип владельца (созданный `new`).
fn replace_uninit(state: &mut State, to: &VerificationType) {
for slot in state.locals.iter_mut().chain(state.stack.iter_mut()) {
if matches!(slot, VerificationType::Uninitialized(_)) {
*slot = to.clone();
}
}
}
fn initialized(class: &str) -> VerificationType {
VerificationType::Object(class.to_string())
}
pub(super) fn context(class: &str, method: &str, error: DescError) -> ClassfileError {
super::super::invalid(class, method, &error.0)
}
pub(super) fn prim_slots(prim: Prim) -> u32 {
match prim {
Prim::Int | Prim::Float => 1,
Prim::Long | Prim::Double => 2,
}
}
pub(super) fn prim_push(prim: Prim) -> Vec<VerificationType> {
match VerificationType::of_prim(prim) {
value @ (VerificationType::Long | VerificationType::Double) => {
vec![value, VerificationType::Top]
}
value => vec![value],
}
}
/// `(слоты аргумента, примитив результата)` для конверсии.
pub(super) fn convert_shapes(op: Convert) -> (usize, Prim) {
use Convert::{D2F, D2I, D2L, F2D, F2I, F2L, I2B, I2C, I2D, I2F, I2L, I2S, L2D, L2F, L2I};
use Prim::{Double, Float, Int, Long};
match op {
I2L => (1, Long),
I2D => (1, Double),
I2F | I2B | I2C | I2S => (1, if op == I2F { Float } else { Int }),
L2I | L2F => (2, if op == L2F { Float } else { Int }),
L2D => (2, Double),
F2I | F2L => (1, if op == F2L { Long } else { Int }),
F2D => (1, Double),
D2I | D2L => (2, if op == D2L { Long } else { Int }),
D2F => (2, Float),
}
}

View file

@ -0,0 +1,233 @@
//! Состояние типов и transfer-функции инструкций.
//!
//! Стек растёт вправо (`push` — в конец). `long`/`double` занимают два слота:
//! значение + `Top`. Проверяется только ФОРМА (глубина, категории), не
//! соответствие типов — неверные типы честно упадут на верификаторе JVM.
pub(super) mod calls;
pub(super) mod stack;
use super::merge::descs::{array_element, field_slots, method_slots};
use super::{VerificationType, invalid};
use crate::error::ClassfileError;
use crate::insn::{Arith, ArrayKind, BranchCond, Insn, SlotKind};
use calls::{const_slots, context, convert_shapes, count_slots, invoke, prim_push, prim_slots};
use stack::{dup, dup2, local_get, pop_slots, push, store_local, wide_push};
/// Типы locals + operand stack в одной точке программы.
#[derive(Debug, Clone, Default, PartialEq)]
pub(super) struct State {
pub locals: Vec<VerificationType>,
pub stack: Vec<VerificationType>,
}
/// Применить инструкцию к состоянию (на месте).
pub(super) fn transfer(
class: &str,
method: &str,
insn: &Insn,
offset: u16,
state: &mut State,
) -> Result<(), ClassfileError> {
match insn {
Insn::Mark(label) => Err(invalid(class, method, &format!("маркер {label} не снят"))),
Insn::Nop => Ok(()),
Insn::AconstNull => push(state, vec![VerificationType::Null]),
Insn::Const(value) => {
let slots = const_slots(value).map_err(|e| context(class, method, e))?;
push(state, slots)
}
Insn::Load { slot, kind } => {
let value = local_get(state, *slot, class, method)?;
let _ = kind;
push(state, wide_push(value))
}
Insn::Store { slot, kind } => {
let slots = usize::from(*kind == SlotKind::Long || *kind == SlotKind::Double) + 1;
let taken = pop_slots(state, slots, class, method, "store")?;
store_local(state, *slot, *kind, taken, class, method)
}
Insn::Iinc { index, .. } => {
let current = local_get(state, *index, class, method)?;
if current != VerificationType::Integer {
return Err(invalid(class, method, "iinc не по int"));
}
Ok(())
}
Insn::ArrayLoad(kind) => {
pop_slots(state, 1, class, method, "индекс массива")?;
let array = pop_slots(state, 1, class, method, "массив")?.remove(0);
push(state, vec![array_element(&array, *kind)])
}
Insn::ArrayStore(kind) => {
let count = match kind {
ArrayKind::Long | ArrayKind::Double => 2,
_ => 1,
};
pop_slots(state, count, class, method, "значение")?;
pop_slots(state, 1, class, method, "индекс массива")?;
pop_slots(state, 1, class, method, "массив")?;
Ok(())
}
Insn::Pop => {
let top = pop_slots(state, 1, class, method, "pop")?.remove(0);
if top == VerificationType::Top {
return Err(invalid(class, method, "pop половины long/double"));
}
Ok(())
}
Insn::Pop2 => {
pop_slots(state, 2, class, method, "pop2")?;
Ok(())
}
Insn::Dup => dup(state, class, method, &[1], &[1, 1]),
Insn::DupX1 => dup(state, class, method, &[2, 1], &[1, 2, 1]),
Insn::DupX2 => dup(state, class, method, &[3, 2, 1], &[1, 3, 2, 1]),
Insn::Dup2 => dup2(state, class, method, false, false),
Insn::Dup2X1 => dup2(state, class, method, true, false),
Insn::Dup2X2 => dup2(state, class, method, true, true),
Insn::Swap => {
let mut taken = pop_slots(state, 2, class, method, "swap")?;
if taken.contains(&VerificationType::Top) {
return Err(invalid(class, method, "swap с половиной"));
}
taken.reverse();
push(state, taken)
}
Insn::Binary { prim, op } => {
let slots = prim_slots(*prim);
if *op == Arith::Neg {
pop_slots(state, slots as usize, class, method, "neg")?;
} else if matches!(op, Arith::Shl | Arith::Shr | Arith::Ushr) {
// Сдвиг берёт (значение, int), а не два значения.
pop_slots(state, slots as usize + 1, class, method, "shift")?;
} else {
pop_slots(state, 2 * slots as usize, class, method, "arith")?;
}
push(state, prim_push(*prim))
}
Insn::Cmp { prim, .. } => {
pop_slots(state, 2 * prim_slots(*prim) as usize, class, method, "cmp")?;
push(state, vec![VerificationType::Integer])
}
Insn::Convert(op) => {
let (src, dst) = convert_shapes(*op);
pop_slots(state, src, class, method, "convert")?;
push(state, prim_push(dst))
}
Insn::Branch(cond, _) => {
let count = match cond {
BranchCond::Zero(..) | BranchCond::Null { .. } => 1,
BranchCond::Int(..) | BranchCond::Ref { .. } => 2,
};
pop_slots(state, count, class, method, "branch")?;
Ok(())
}
Insn::Goto(_) => Ok(()),
Insn::Jsr(_) | Insn::Ret(_) => Err(invalid(
class,
method,
"подпрограммы jsr/ret не поддерживаются",
)),
Insn::TableSwitch { .. } | Insn::LookupSwitch { .. } => {
pop_slots(state, 1, class, method, "switch")?;
Ok(())
}
Insn::Return(prim) => {
if let Some(prim) = prim {
pop_slots(state, prim_slots(*prim) as usize, class, method, "return")?;
}
Ok(())
}
Insn::AReturn => {
pop_slots(state, 1, class, method, "areturn")?;
Ok(())
}
Insn::GetStatic { descriptor, .. } => {
let slots = field_slots(descriptor).map_err(|e| context(class, method, e))?;
push(state, slots)
}
Insn::PutStatic { descriptor, .. } => {
let slots = field_slots(descriptor).map_err(|e| context(class, method, e))?;
pop_slots(state, slots.len(), class, method, "putstatic")?;
Ok(())
}
Insn::GetField { descriptor, .. } => {
pop_slots(state, 1, class, method, "objectref")?;
let slots = field_slots(descriptor).map_err(|e| context(class, method, e))?;
push(state, slots)
}
Insn::PutField { descriptor, .. } => {
let slots = field_slots(descriptor).map_err(|e| context(class, method, e))?;
pop_slots(state, slots.len(), class, method, "putfield")?;
pop_slots(state, 1, class, method, "objectref")?;
Ok(())
}
Insn::Invoke {
kind,
owner,
name,
descriptor,
..
} => invoke(class, method, state, *kind, owner, name, descriptor),
Insn::InvokeDynamic { descriptor, .. } => {
let (params, ret) = method_slots(descriptor).map_err(|e| context(class, method, e))?;
pop_slots(state, count_slots(&params), class, method, "invokedynamic")?;
if let Some(ret) = ret {
push(state, ret)?;
}
Ok(())
}
Insn::New(_) => push(state, vec![VerificationType::Uninitialized(offset)]),
Insn::NewArray(kind) => {
pop_slots(state, 1, class, method, "newarray")?;
let descriptor = match kind {
ArrayKind::Boolean => "[Z",
ArrayKind::Char => "[C",
ArrayKind::Float => "[F",
ArrayKind::Double => "[D",
ArrayKind::Byte => "[B",
ArrayKind::Short => "[S",
ArrayKind::Int => "[I",
ArrayKind::Long => "[J",
ArrayKind::Reference => {
return Err(invalid(class, method, "newarray ссылочного типа"));
}
};
push(
state,
vec![VerificationType::Object(descriptor.to_string())],
)
}
Insn::ANewArray(name) => {
pop_slots(state, 1, class, method, "anewarray")?;
push(state, vec![VerificationType::Object(format!("[L{name};"))])
}
Insn::MultiANewArray { descriptor, dims } => {
pop_slots(state, *dims as usize, class, method, "multianewarray")?;
push(state, vec![VerificationType::Object(descriptor.clone())])
}
Insn::ArrayLength => {
pop_slots(state, 1, class, method, "arraylength")?;
push(state, vec![VerificationType::Integer])
}
Insn::AThrow => {
pop_slots(state, 1, class, method, "athrow")?;
state.stack.clear();
Ok(())
}
Insn::CheckCast(name) => {
pop_slots(state, 1, class, method, "checkcast")?;
push(state, vec![VerificationType::Object(name.clone())])
}
Insn::InstanceOf(_) => {
pop_slots(state, 1, class, method, "instanceof")?;
push(state, vec![VerificationType::Integer])
}
Insn::MonitorEnter | Insn::MonitorExit => {
pop_slots(state, 1, class, method, "monitor")?;
Ok(())
}
}
}

View file

@ -0,0 +1,161 @@
//! Стек и locals: снятие/укладка слотов, `dup*`.
use super::super::{VerificationType, invalid};
use super::State;
use crate::error::ClassfileError;
use crate::insn::SlotKind;
/// Запись в local с учётом ширины (`long`/`double` — парой с `Top`).
pub(super) fn store_local(
state: &mut State,
slot: u16,
kind: SlotKind,
taken: Vec<VerificationType>,
class: &str,
method: &str,
) -> Result<(), ClassfileError> {
let wide = kind == SlotKind::Long || kind == SlotKind::Double;
if taken.len() != usize::from(wide) + 1 {
return Err(invalid(class, method, "ширина store не совпала"));
}
let mut taken = taken;
local_set(state, slot, taken.remove(0), class, method)?;
if wide {
local_set(
state,
slot.checked_add(1)
.ok_or_else(|| invalid(class, method, "слот вне u16"))?,
VerificationType::Top,
class,
method,
)?;
}
Ok(())
}
pub(super) fn push(state: &mut State, slots: Vec<VerificationType>) -> Result<(), ClassfileError> {
state.stack.extend(slots);
Ok(())
}
/// Снять `n` слотов (снизу вверх); нехватка — честная ошибка.
pub(super) fn pop_slots(
state: &mut State,
n: usize,
class: &str,
method: &str,
what: &str,
) -> Result<Vec<VerificationType>, ClassfileError> {
if state.stack.len() < n {
return Err(invalid(
class,
method,
&format!("переполнение стека вниз в {what}"),
));
}
Ok(state.stack.split_off(state.stack.len() - n))
}
pub(super) fn local_get(
state: &State,
slot: u16,
class: &str,
method: &str,
) -> Result<VerificationType, ClassfileError> {
state
.locals
.get(slot as usize)
.cloned()
.ok_or_else(|| invalid(class, method, &format!("чтение слота {slot} вне locals")))
}
pub(super) fn local_set(
state: &mut State,
slot: u16,
value: VerificationType,
class: &str,
method: &str,
) -> Result<(), ClassfileError> {
let cell = state
.locals
.get_mut(slot as usize)
.ok_or_else(|| invalid(class, method, &format!("запись слота {slot} вне locals")))?;
*cell = value;
Ok(())
}
/// Значение local'а целиком: `long`/`double` — парой с `Top`.
pub(super) fn wide_push(value: VerificationType) -> Vec<VerificationType> {
match value {
VerificationType::Long | VerificationType::Double => vec![value, VerificationType::Top],
_ => vec![value],
}
}
/// `dup*` над значениями-категории-1: снять по позициям `take`, вернуть `put`.
pub(super) fn dup(
state: &mut State,
class: &str,
method: &str,
take: &[usize],
put: &[usize],
) -> Result<(), ClassfileError> {
let depth = take.iter().max().copied().unwrap_or(0);
let taken = pop_slots(state, depth, class, method, "dup")?;
if taken.contains(&VerificationType::Top) {
return Err(invalid(class, method, "dup над половиной"));
}
let mut out = Vec::with_capacity(put.len());
for position in put {
out.push(taken[depth - position].clone());
}
push(state, out)
}
/// `dup2*`: форма выбирается по вершине (`Top` — вторая половина `long`).
pub(super) fn dup2(
state: &mut State,
class: &str,
method: &str,
deep: bool,
deeper: bool,
) -> Result<(), ClassfileError> {
let wide = state.stack.last() == Some(&VerificationType::Top);
if wide {
let depth = if deeper {
4
} else if deep {
3
} else {
2
};
let taken = pop_slots(state, depth, class, method, "dup2")?;
if taken[depth - 1] != VerificationType::Top {
return Err(invalid(class, method, "dup2: нет половины"));
}
let value = taken[depth - 2].clone();
if !matches!(value, VerificationType::Long | VerificationType::Double) {
return Err(invalid(class, method, "dup2: половина без long/double"));
}
let mut out = vec![value, VerificationType::Top];
out.extend(taken);
push(state, out)
} else {
let depth = if deeper {
4
} else if deep {
3
} else {
2
};
let taken = pop_slots(state, depth, class, method, "dup2")?;
if taken.contains(&VerificationType::Top) {
return Err(invalid(class, method, "dup2 над половиной"));
}
let (head, tail) = taken.split_at(depth - 2);
let mut out = tail.to_vec();
out.extend_from_slice(head);
out.extend_from_slice(tail);
push(state, out)
}
}

View file

@ -0,0 +1,29 @@
//! Чтение/запись бинарного формата `.class` (JVMS §4) через backend
//! `ristretto_classfile`: обзор кандидатов и решение — `docs/stack.md`.
mod frames;
mod parse;
pub(crate) mod write;
pub use frames::{Hierarchy, StackMapFrame, VerificationType, compute_frames};
use crate::error::ClassfileError;
use crate::model::ClassFile;
/// Разобрать один `.class` в [`ClassFile`].
pub fn parse(bytes: &[u8]) -> Result<ClassFile, ClassfileError> {
parse::class::parse_class(bytes)
}
/// Записать [`ClassFile`] в байты, включая пересчёт `Code`/`StackMapTable`.
pub fn write(class: &ClassFile) -> Result<Vec<u8>, ClassfileError> {
write::write_class(class)
}
/// Классы, которые не изменялись, должны уезжать в выходной архив «как есть».
///
/// Это дешевле и безопаснее, чем round-trip через наш IR: нет риска потерять
/// неизвестный атрибут. Реализация зависит от решения по backend (P1).
pub fn passthrough_needed(class: &ClassFile) -> bool {
class.methods.is_empty() && class.fields.is_empty()
}

View file

@ -0,0 +1,204 @@
//! Атрибуты backend'а → атрибуты нашего IR.
//!
//! `BootstrapMethods`, `EnclosingMethod`, `MethodParameters` и `ConstantValue`
//! моделируются явно (в них лежат ссылки пула, переживающие пересборку);
//! остальное структурированное, но нам ненужное, уезжает в `Raw` как есть.
//! `Code` и `StackMapTable` здесь не обрабатываются — их разбирает вызывающий
//! код метода (см. `super::class`).
use ristretto_classfile::ConstantPool;
use ristretto_classfile::attributes::Attribute as RtAttr;
use super::pool::{
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};
/// Один атрибут backend'а → наш атрибут (кроме `Code`/`StackMapTable`).
pub(super) fn convert(attr: &RtAttr, pool: &ConstantPool<'_>) -> Result<Attribute, ClassfileError> {
Ok(match attr {
RtAttr::SourceFile {
source_file_index, ..
} => Attribute::SourceFile(utf8(pool, *source_file_index)?),
RtAttr::Signature {
signature_index, ..
} => Attribute::Signature(utf8(pool, *signature_index)?),
RtAttr::Exceptions {
exception_indexes, ..
} => Attribute::Exceptions(
exception_indexes
.iter()
.map(|index| class_name(pool, *index))
.collect::<Result<_, _>>()?,
),
RtAttr::InnerClasses { classes, .. } => Attribute::InnerClasses(
classes
.iter()
.map(|class| inner(pool, class))
.collect::<Result<_, _>>()?,
),
RtAttr::EnclosingMethod {
class_index,
method_index,
..
} => Attribute::EnclosingMethod {
class: class_name(pool, *class_index)?,
method: enclosing_method(pool, *method_index)?,
},
RtAttr::MethodParameters { parameters, .. } => Attribute::MethodParameters(
parameters
.iter()
.map(|param| method_param(pool, param))
.collect::<Result<_, _>>()?,
),
RtAttr::BootstrapMethods { methods, .. } => Attribute::BootstrapMethods(
methods
.iter()
.map(|method| bootstrap(pool, method))
.collect::<Result<_, _>>()?,
),
RtAttr::ConstantValue {
constant_value_index,
..
} => Attribute::ConstantValue(const_from(pool, *constant_value_index)?),
RtAttr::Deprecated { .. } => Attribute::Deprecated,
RtAttr::Synthetic { .. } => Attribute::Synthetic,
RtAttr::LineNumberTable { .. }
| RtAttr::LocalVariableTable { .. }
| RtAttr::LocalVariableTypeTable { .. } => {
return Err(ClassfileError::Parse(
"таблицы кода разбирает вызывающий код метода".into(),
));
}
RtAttr::Record { records, .. } => Attribute::Record(
records
.iter()
.map(|record| {
Ok(RecordComponent {
name: utf8(pool, record.name_index)?,
descriptor: utf8(pool, record.descriptor_index)?,
attributes: record
.attributes
.iter()
.map(|attr| convert(attr, pool))
.collect::<Result<_, _>>()?,
})
})
.collect::<Result<_, ClassfileError>>()?,
),
RtAttr::Code { .. } | RtAttr::StackMapTable { .. } => {
return Err(ClassfileError::Parse(
"Code/StackMapTable разбирает вызывающий код метода".into(),
));
}
other => raw(other)?,
})
}
/// Несмоделированный атрибут → `Raw`: имя + байты `info` (без заголовка).
pub(super) fn raw(attr: &RtAttr) -> Result<Attribute, ClassfileError> {
let mut bytes = Vec::new();
attr.to_bytes(&mut bytes)
.map_err(|e| ClassfileError::Parse(e.to_string()))?;
let info = bytes
.get(6..)
.ok_or_else(|| ClassfileError::Parse("короткая запись атрибута".into()))?;
Ok(Attribute::Raw {
name: attr.name().to_string(),
data: info.to_vec(),
})
}
fn inner(
pool: &ConstantPool<'_>,
class: &ristretto_classfile::attributes::InnerClass,
) -> Result<InnerClass, ClassfileError> {
Ok(InnerClass {
inner: class_name(pool, class.class_info_index)?,
outer: optional_class(pool, class.outer_class_info_index)?,
inner_name: optional_utf8(pool, class.name_index)?,
access_flags: class.access_flags.bits(),
})
}
/// Метод объемлющего класса (`0` — только класс, без метода).
fn enclosing_method(
pool: &ConstantPool<'_>,
index: u16,
) -> Result<Option<(String, String)>, ClassfileError> {
if index == 0 {
Ok(None)
} else {
Ok(Some(name_and_type(pool, index)?))
}
}
fn method_param(
pool: &ConstantPool<'_>,
param: &ristretto_classfile::attributes::MethodParameter,
) -> Result<MethodParameter, ClassfileError> {
Ok(MethodParameter {
name: optional_utf8(pool, param.name_index)?,
access_flags: param.access_flags.bits(),
})
}
fn bootstrap(
pool: &ConstantPool<'_>,
method: &ristretto_classfile::attributes::BootstrapMethod,
) -> Result<BootstrapMethod, ClassfileError> {
let constant = pool
.try_get(method.bootstrap_method_ref)
.map_err(|e| ClassfileError::Parse(e.to_string()))?;
let (kind, owner, name, descriptor, itf) = match constant {
ristretto_classfile::Constant::MethodHandle {
reference_kind,
reference_index,
} => {
let (owner, name, descriptor) = ref_parts(pool, *reference_index)?;
(
kind_code(reference_kind),
owner,
name,
descriptor,
is_interface(pool, *reference_index),
)
}
other => {
return Err(ClassfileError::Parse(format!(
"bootstrap-ссылка обязана быть MethodHandle, найден {other:?}"
)));
}
};
Ok(BootstrapMethod {
kind,
owner,
name,
descriptor,
itf,
args: method
.arguments
.iter()
.map(|index| const_from(pool, *index))
.collect::<Result<_, _>>()?,
})
}
/// Индекс пула, где `0` означает отсутствие значения.
fn optional_class(pool: &ConstantPool<'_>, index: u16) -> Result<Option<String>, ClassfileError> {
if index == 0 {
Ok(None)
} else {
Ok(Some(class_name(pool, index)?))
}
}
/// Индекс `CONSTANT_Utf8`, где `0` означает отсутствие значения.
fn optional_utf8(pool: &ConstantPool<'_>, index: u16) -> Result<Option<String>, ClassfileError> {
if index == 0 {
Ok(None)
} else {
Ok(Some(utf8(pool, index)?))
}
}

View file

@ -0,0 +1,109 @@
//! Класс целиком: backend `ClassFile` → наш [`ClassFile`](crate::model::ClassFile).
use ristretto_classfile::attributes::Attribute as RtAttr;
use ristretto_classfile::{
ClassFile as RtClass, ConstantPool, Field as RtField, Method as RtMethod,
};
use super::attrs;
use super::code::convert_code;
use super::pool::{class_name, utf8};
use crate::error::ClassfileError;
use crate::model::{Attribute, ClassFile, Code, Field, Method, Version};
/// Разобрать байты `.class` в наш IR.
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 pool = &rt.constant_pool;
let interfaces = rt
.interfaces
.iter()
.map(|index| class_name(pool, *index))
.collect::<Result<_, _>>()?;
Ok(ClassFile {
version: Version {
minor: rt.version.minor(),
major: rt.version.major(),
},
access_flags: rt.access_flags.bits(),
this_class: class_name(pool, rt.this_class)?,
super_class: optional_class(pool, rt.super_class)?,
interfaces,
fields: rt
.fields
.iter()
.map(|field| convert_field(field, pool))
.collect::<Result<_, _>>()?,
methods: rt
.methods
.iter()
.map(|method| convert_method(method, pool))
.collect::<Result<_, _>>()?,
attributes: convert_attrs(&rt.attributes, pool)?,
})
}
fn convert_field(field: &RtField, pool: &ConstantPool<'_>) -> Result<Field, ClassfileError> {
Ok(Field {
access_flags: field.access_flags.bits(),
name: utf8(pool, field.name_index)?,
descriptor: utf8(pool, field.descriptor_index)?,
attributes: convert_attrs(&field.attributes, pool)?,
})
}
fn convert_method(method: &RtMethod, pool: &ConstantPool<'_>) -> Result<Method, ClassfileError> {
let mut code: Option<Code> = None;
let mut rest: Vec<Attribute> = Vec::new();
for attr in &method.attributes {
match attr {
RtAttr::Code {
max_stack,
max_locals,
code: insns,
exception_table,
attributes,
..
} => {
if code.is_some() {
return Err(ClassfileError::Parse("два Code в одном методе".into()));
}
code = Some(convert_code(
*max_stack,
*max_locals,
insns,
exception_table,
attributes,
pool,
)?);
}
other => rest.push(attrs::convert(other, pool)?),
}
}
Ok(Method {
access_flags: method.access_flags.bits(),
name: utf8(pool, method.name_index)?,
descriptor: utf8(pool, method.descriptor_index)?,
code,
attributes: rest,
})
}
fn convert_attrs(
attrs: &[RtAttr],
pool: &ConstantPool<'_>,
) -> Result<Vec<Attribute>, ClassfileError> {
attrs
.iter()
.map(|attr| attrs::convert(attr, pool))
.collect()
}
/// Индекс суперкласса, где `0` — его отсутствие (`java/lang/Object`).
fn optional_class(pool: &ConstantPool<'_>, index: u16) -> Result<Option<String>, ClassfileError> {
if index == 0 {
Ok(None)
} else {
Ok(Some(class_name(pool, index)?))
}
}

View file

@ -0,0 +1,112 @@
//! Кадры `StackMapTable` при чтении: вариант в вариант, дельты как есть.
//!
//! Backend хранит кадры в номерах инструкций (см. `from_bytes_code_attributes`),
//! наш IR — тоже: writer сам пересчитает их в байты при `to_bytes`.
use ristretto_classfile::ConstantPool;
use ristretto_classfile::attributes::{StackFrame, VerificationType as RtType};
use super::super::pool::class_name;
use crate::error::ClassfileError;
use crate::format::{StackMapFrame, VerificationType};
/// Кадры backend'а → наши (индексные дельты — как есть).
pub(super) fn to_frames(
frames: &[StackFrame],
pool: &ConstantPool<'_>,
) -> Result<Vec<StackMapFrame>, ClassfileError> {
frames.iter().map(|frame| to_frame(frame, pool)).collect()
}
fn to_frame(frame: &StackFrame, pool: &ConstantPool<'_>) -> Result<StackMapFrame, ClassfileError> {
Ok(match frame {
StackFrame::SameFrame { frame_type } => StackMapFrame::Same {
offset_delta: u32::from(*frame_type),
},
StackFrame::SameFrameExtended { offset_delta, .. } => StackMapFrame::Same {
offset_delta: u32::from(*offset_delta),
},
StackFrame::SameLocals1StackItemFrame { frame_type, stack } => StackMapFrame::SameLocals {
offset_delta: u32::from(*frame_type) - 64,
local: single(stack, pool)?,
},
StackFrame::SameLocals1StackItemFrameExtended {
offset_delta,
stack,
..
} => StackMapFrame::SameLocals {
offset_delta: u32::from(*offset_delta),
local: single(stack, pool)?,
},
StackFrame::ChopFrame {
frame_type,
offset_delta,
} => StackMapFrame::Chop {
offset_delta: u32::from(*offset_delta),
count: 251u8.wrapping_sub(*frame_type),
},
StackFrame::AppendFrame {
offset_delta,
locals,
..
} => StackMapFrame::Append {
offset_delta: u32::from(*offset_delta),
locals: types(locals, pool)?,
},
StackFrame::FullFrame {
offset_delta,
locals,
stack,
..
} => StackMapFrame::Full {
offset_delta: u32::from(*offset_delta),
locals: types(locals, pool)?,
stack: types(stack, pool)?,
},
})
}
/// Единственный элемент стека кадра (`same_locals_1_stack_item`).
fn single(stack: &[RtType], pool: &ConstantPool<'_>) -> Result<VerificationType, ClassfileError> {
let mut types = types(stack, pool)?.into_iter();
match (types.next(), types.next()) {
(Some(first), None) => Ok(first),
_ => Err(ClassfileError::Parse(
"в same_locals-кадре не один элемент стека".into(),
)),
}
}
fn types(
values: &[RtType],
pool: &ConstantPool<'_>,
) -> Result<Vec<VerificationType>, ClassfileError> {
values.iter().map(|value| vtype(value, pool)).collect()
}
/// Тип верификации backend'а → наш тип (имя класса берётся из constant pool).
fn vtype(value: &RtType, pool: &ConstantPool<'_>) -> Result<VerificationType, ClassfileError> {
Ok(match value {
RtType::Top => VerificationType::Top,
RtType::Integer => VerificationType::Integer,
RtType::Float => VerificationType::Float,
RtType::Double => VerificationType::Double,
RtType::Long => VerificationType::Long,
RtType::Null => VerificationType::Null,
RtType::UninitializedThis => VerificationType::UninitializedThis,
RtType::Object { cpool_index } => VerificationType::Object(class_name(pool, *cpool_index)?),
RtType::Uninitialized { offset } => VerificationType::Uninitialized(*offset),
})
}
/// Тип перехвата (`0` — catch-all для `finally`/`any`).
pub(super) fn catch_type(
pool: &ConstantPool<'_>,
index: u16,
) -> Result<Option<String>, ClassfileError> {
if index == 0 {
Ok(None)
} else {
Ok(Some(class_name(pool, index)?))
}
}

View file

@ -0,0 +1,187 @@
//! Метки тела метода: где обязаны стоять и как резолвятся.
//!
//! Backend нумерует инструкции по порядку — метки ставятся прямо на номера,
//! без байтовых смещений.
use std::collections::{BTreeMap, BTreeSet};
use ristretto_classfile::attributes::Attribute as RtAttr;
use ristretto_classfile::attributes::{ExceptionTableEntry, Instruction};
use crate::error::ClassfileError;
use crate::insn::Label;
/// Номера инструкций, на которых обязана стоять метка.
///
/// `extra` — позиции отладочных таблиц (уже номерами): вставки пассов
/// не должны оставлять их без меток.
pub(super) fn label_indexes(
code: &[Instruction],
exception_table: &[ExceptionTableEntry],
extra: &BTreeSet<u32>,
) -> Result<BTreeSet<u32>, ClassfileError> {
let mut indexes = BTreeSet::from([0u32]);
for (position, instruction) in code.iter().enumerate() {
let index = to_index(position)?;
for target in branch_indexes(instruction, index) {
indexes.insert(to_index_checked(target)?);
}
}
for entry in exception_table {
indexes.insert(u32::from(entry.range_pc.start));
indexes.insert(u32::from(entry.range_pc.end));
indexes.insert(u32::from(entry.handler_pc));
}
indexes.extend(extra.iter().copied());
Ok(indexes)
}
/// Цели ветвлений backend'а: для `if*`/`goto` — абсолютный номер, для `switch` — дельта.
fn branch_indexes(instruction: &Instruction, index: u16) -> Vec<i32> {
let base = i32::from(index);
match instruction {
Instruction::Tableswitch(table) => once(base + table.default)
.chain(table.offsets.iter().map(|target| base + target))
.collect(),
Instruction::Lookupswitch(lookup) => once(base + lookup.default)
.chain(lookup.pairs.values().map(|target| base + target))
.collect(),
Instruction::Ifeq(target)
| Instruction::Ifne(target)
| Instruction::Iflt(target)
| Instruction::Ifge(target)
| Instruction::Ifgt(target)
| Instruction::Ifle(target)
| Instruction::If_icmpeq(target)
| Instruction::If_icmpne(target)
| Instruction::If_icmplt(target)
| Instruction::If_icmpge(target)
| Instruction::If_icmpgt(target)
| Instruction::If_icmple(target)
| Instruction::If_acmpeq(target)
| Instruction::If_acmpne(target)
| Instruction::Ifnull(target)
| Instruction::Ifnonnull(target)
| Instruction::Goto(target)
| Instruction::Jsr(target) => vec![i32::from(*target)],
Instruction::Goto_w(target) | Instruction::Jsr_w(target) => vec![*target],
_ => Vec::new(),
}
}
fn once(value: i32) -> std::iter::Once<i32> {
std::iter::once(value)
}
/// Превратить отсортированный набор номеров в последовательные метки.
pub(super) fn assign(indexes: &BTreeSet<u32>) -> BTreeMap<u32, Label> {
indexes
.iter()
.enumerate()
.map(|(position, index)| (*index, position as u32))
.collect()
}
/// Метка для номера инструкции.
pub(super) fn lookup(labels: &BTreeMap<u32, Label>, index: u16) -> Result<Label, ClassfileError> {
labels
.get(&u32::from(index))
.copied()
.ok_or_else(|| ClassfileError::Parse(format!("нет метки для инструкции {index}")))
}
/// Позиции отладочных таблиц, которым нужны метки.
pub(super) fn collect_debug_labels(
attributes: &[RtAttr],
starts: &[u16],
code_length: u16,
extra: &mut BTreeSet<u32>,
) -> Result<(), ClassfileError> {
for attr in attributes {
match attr {
RtAttr::LineNumberTable { line_numbers, .. } => {
for line in line_numbers {
extra.insert(u32::from(line.start_pc));
}
}
RtAttr::LocalVariableTable { variables, .. } => {
for var in variables {
extra.insert(byte_to_index(starts, var.start_pc)?);
extra.insert(end_index(starts, var.start_pc, var.length, code_length)?);
}
}
RtAttr::LocalVariableTypeTable { variable_types, .. } => {
for var in variable_types {
extra.insert(byte_to_index(starts, var.start_pc)?);
extra.insert(end_index(starts, var.start_pc, var.length, code_length)?);
}
}
_ => {}
}
}
Ok(())
}
/// Байт → номер инструкции (только границы инструкций).
pub(super) fn byte_to_index(starts: &[u16], byte: u16) -> Result<u32, ClassfileError> {
starts
.iter()
.position(|&start| start == byte)
.and_then(|position| u32::try_from(position).ok())
.ok_or_else(|| ClassfileError::Parse(format!("смещение {byte} мимо инструкции")))
}
/// Конец диапазона `[start, start+length)`: индекс за концом.
fn end_index(
starts: &[u16],
start: u16,
length: u16,
code_length: u16,
) -> Result<u32, ClassfileError> {
let end = start
.checked_add(length)
.ok_or_else(|| ClassfileError::Parse("диапазон вне метода".into()))?;
if end == code_length {
return u32::try_from(starts.len())
.map_err(|_| ClassfileError::Parse("в теле метода больше 65535 инструкций".into()));
}
byte_to_index(starts, end)
}
/// Метка для байтового смещения отладочной таблицы.
pub(super) fn resolve_byte(
labels: &BTreeMap<u32, Label>,
starts: &[u16],
byte: u16,
) -> Result<Label, ClassfileError> {
let index = byte_to_index(starts, byte)?;
labels
.get(&index)
.copied()
.ok_or_else(|| ClassfileError::Parse(format!("нет метки для смещения {byte}")))
}
/// Метка конца диапазона отладочной таблицы.
pub(super) fn resolve_end(
labels: &BTreeMap<u32, Label>,
starts: &[u16],
start: u16,
length: u16,
code_length: u16,
) -> Result<Label, ClassfileError> {
let index = end_index(starts, start, length, code_length)?;
labels
.get(&index)
.copied()
.ok_or_else(|| ClassfileError::Parse(format!("нет метки конца {index}")))
}
fn to_index(position: usize) -> Result<u16, ClassfileError> {
u16::try_from(position)
.map_err(|_| ClassfileError::Parse("в теле метода больше 65535 инструкций".into()))
}
fn to_index_checked(index: i32) -> Result<u32, ClassfileError> {
u32::try_from(index)
.map_err(|_| ClassfileError::Parse(format!("номер инструкции {index} вне тела метода")))
}

View file

@ -0,0 +1,169 @@
//! `Code` backend'а → тело метода нашего IR: метки, исключения, кадры.
//!
//! Backend уже перевёл байтовые смещения в номера инструкций — конвертеры
//! работают прямо с ними: метки ставятся на целях переходов, границах
//! try-блоков и позициях отладочных таблиц; кадры переносятся как есть
//! (индексные дельты). Позиции `LineNumberTable`/`LocalVariableTable` —
//! тоже метки, поэтому вставки пассов их не ломают (байты досчитает writer).
mod frames;
mod labels;
use std::collections::BTreeSet;
use std::io::Cursor;
use ristretto_classfile::ConstantPool;
use ristretto_classfile::attributes::Attribute as RtAttr;
use ristretto_classfile::attributes::{ExceptionTableEntry, Instruction};
use super::attrs;
use super::insns::convert;
use crate::error::ClassfileError;
use crate::insn::Insn;
use crate::model::{
Attribute, Code, ExceptionHandler, LineNumber, LocalVariable, LocalVariableType,
};
use frames::{catch_type, to_frames};
use labels::{assign, collect_debug_labels, label_indexes, lookup, resolve_byte, resolve_end};
/// Тело метода целиком: backend `Code` → наш [`Code`].
pub(super) fn convert_code(
max_stack: u16,
max_locals: u16,
code: &[Instruction],
exception_table: &[ExceptionTableEntry],
attributes: &[RtAttr],
pool: &ConstantPool<'_>,
) -> Result<Code, ClassfileError> {
let (starts, code_length) = layout(code)?;
let mut extra = BTreeSet::new();
collect_debug_labels(attributes, &starts, code_length, &mut extra)?;
let labels = assign(&label_indexes(code, exception_table, &extra)?);
let resolve = |index: u16| lookup(&labels, index);
let mut insns = Vec::with_capacity(code.len() + labels.len());
for (position, instruction) in code.iter().enumerate() {
let index = u16::try_from(position)
.map_err(|_| ClassfileError::Parse("в теле метода больше 65535 инструкций".into()))?;
if let Some(label) = labels.get(&u32::from(index)) {
insns.push(Insn::Mark(*label));
}
insns.push(convert(instruction, index, pool, &resolve)?);
}
let end = u32::try_from(code.len())
.map_err(|_| ClassfileError::Parse("в теле метода больше 65535 инструкций".into()))?;
if let Some(label) = labels.get(&end) {
insns.push(Insn::Mark(*label));
}
let exception_table = exception_table
.iter()
.map(|entry| {
Ok(ExceptionHandler {
start: lookup(&labels, entry.range_pc.start)?,
end: lookup(&labels, entry.range_pc.end)?,
handler: lookup(&labels, entry.handler_pc)?,
catch_type: catch_type(pool, entry.catch_type)?,
})
})
.collect::<Result<_, ClassfileError>>()?;
let mut frames = Vec::new();
let mut rest = Vec::new();
for attr in attributes {
match attr {
RtAttr::StackMapTable { frames: found, .. } => {
if !frames.is_empty() {
return Err(ClassfileError::Parse(
"два StackMapTable в одном Code".into(),
));
}
frames = to_frames(found, pool)?;
}
RtAttr::LineNumberTable { line_numbers, .. } => rest.push(Attribute::LineNumberTable(
line_numbers
.iter()
.map(|line| {
Ok(LineNumber {
start: lookup(&labels, line.start_pc)?,
line: line.line_number,
})
})
.collect::<Result<_, ClassfileError>>()?,
)),
RtAttr::LocalVariableTable { variables, .. } => {
rest.push(Attribute::LocalVariableTable(
variables
.iter()
.map(|var| {
Ok(LocalVariable {
start: resolve_byte(&labels, &starts, var.start_pc)?,
end: resolve_end(
&labels,
&starts,
var.start_pc,
var.length,
code_length,
)?,
name: super::pool::utf8(pool, var.name_index)?,
descriptor: super::pool::utf8(pool, var.descriptor_index)?,
index: var.index,
})
})
.collect::<Result<_, ClassfileError>>()?,
))
}
RtAttr::LocalVariableTypeTable { variable_types, .. } => {
rest.push(Attribute::LocalVariableTypeTable(
variable_types
.iter()
.map(|var| {
Ok(LocalVariableType {
start: resolve_byte(&labels, &starts, var.start_pc)?,
end: resolve_end(
&labels,
&starts,
var.start_pc,
var.length,
code_length,
)?,
name: super::pool::utf8(pool, var.name_index)?,
signature: super::pool::utf8(pool, var.signature_index)?,
index: var.index,
})
})
.collect::<Result<_, ClassfileError>>()?,
));
}
other => rest.push(attrs::convert(other, pool)?),
}
}
Ok(Code {
max_stack,
max_locals,
insns,
exception_table,
frames,
attributes: rest,
})
}
/// Байтовые смещения инструкций и длина тела метода.
fn layout(code: &[Instruction]) -> Result<(Vec<u16>, u16), ClassfileError> {
let mut cursor = Cursor::new(Vec::new());
let mut offsets = Vec::with_capacity(code.len());
for instruction in code {
offsets.push(tell(&cursor)?);
instruction
.to_bytes(&mut cursor)
.map_err(|e| ClassfileError::Parse(e.to_string()))?;
}
Ok((offsets, tell(&cursor)?))
}
fn tell(cursor: &Cursor<Vec<u8>>) -> Result<u16, ClassfileError> {
u16::try_from(cursor.position())
.map_err(|_| ClassfileError::Parse("тело метода длиннее 64 КБ".into()))
}

View file

@ -0,0 +1,203 @@
//! Простые инструкции без ссылок на пул (кроме `ldc`) и без ветвлений.
use ristretto_classfile::ConstantPool;
use ristretto_classfile::attributes::Instruction;
use super::super::pool::const_from;
use crate::error::ClassfileError;
use crate::insn::{Arith, ArrayKind, Const, Convert, Insn, Prim, SlotKind};
/// `Some` — инструкция покрыта, `None` — отдать следующим конвертерам.
pub(super) fn convert_basic(
instruction: &Instruction,
pool: &ConstantPool<'_>,
) -> Result<Option<Insn>, ClassfileError> {
Ok(Some(match instruction {
Instruction::Nop => Insn::Nop,
Instruction::Aconst_null => Insn::AconstNull,
Instruction::Iconst_m1 => Insn::Const(Const::Int(-1)),
Instruction::Iconst_0 => Insn::Const(Const::Int(0)),
Instruction::Iconst_1 => Insn::Const(Const::Int(1)),
Instruction::Iconst_2 => Insn::Const(Const::Int(2)),
Instruction::Iconst_3 => Insn::Const(Const::Int(3)),
Instruction::Iconst_4 => Insn::Const(Const::Int(4)),
Instruction::Iconst_5 => Insn::Const(Const::Int(5)),
Instruction::Lconst_0 => Insn::Const(Const::Long(0)),
Instruction::Lconst_1 => Insn::Const(Const::Long(1)),
Instruction::Fconst_0 => Insn::Const(Const::Float(0.0)),
Instruction::Fconst_1 => Insn::Const(Const::Float(1.0)),
Instruction::Fconst_2 => Insn::Const(Const::Float(2.0)),
Instruction::Dconst_0 => Insn::Const(Const::Double(0.0)),
Instruction::Dconst_1 => Insn::Const(Const::Double(1.0)),
Instruction::Bipush(v) => Insn::Const(Const::Int(i32::from(*v))),
Instruction::Sipush(v) => Insn::Const(Const::Int(i32::from(*v))),
Instruction::Ldc(i) => Insn::Const(const_from(pool, u16::from(*i))?),
Instruction::Ldc_w(i) | Instruction::Ldc2_w(i) => Insn::Const(const_from(pool, *i)?),
Instruction::Iload(i) => load(u16::from(*i), SlotKind::Int),
Instruction::Lload(i) => load(u16::from(*i), SlotKind::Long),
Instruction::Fload(i) => load(u16::from(*i), SlotKind::Float),
Instruction::Dload(i) => load(u16::from(*i), SlotKind::Double),
Instruction::Aload(i) => load(u16::from(*i), SlotKind::Ref),
Instruction::Iload_w(i) => load(*i, SlotKind::Int),
Instruction::Lload_w(i) => load(*i, SlotKind::Long),
Instruction::Fload_w(i) => load(*i, SlotKind::Float),
Instruction::Dload_w(i) => load(*i, SlotKind::Double),
Instruction::Aload_w(i) => load(*i, SlotKind::Ref),
Instruction::Iload_0 => load(0, SlotKind::Int),
Instruction::Iload_1 => load(1, SlotKind::Int),
Instruction::Iload_2 => load(2, SlotKind::Int),
Instruction::Iload_3 => load(3, SlotKind::Int),
Instruction::Lload_0 => load(0, SlotKind::Long),
Instruction::Lload_1 => load(1, SlotKind::Long),
Instruction::Lload_2 => load(2, SlotKind::Long),
Instruction::Lload_3 => load(3, SlotKind::Long),
Instruction::Fload_0 => load(0, SlotKind::Float),
Instruction::Fload_1 => load(1, SlotKind::Float),
Instruction::Fload_2 => load(2, SlotKind::Float),
Instruction::Fload_3 => load(3, SlotKind::Float),
Instruction::Dload_0 => load(0, SlotKind::Double),
Instruction::Dload_1 => load(1, SlotKind::Double),
Instruction::Dload_2 => load(2, SlotKind::Double),
Instruction::Dload_3 => load(3, SlotKind::Double),
Instruction::Aload_0 => load(0, SlotKind::Ref),
Instruction::Aload_1 => load(1, SlotKind::Ref),
Instruction::Aload_2 => load(2, SlotKind::Ref),
Instruction::Aload_3 => load(3, SlotKind::Ref),
Instruction::Istore(i) => store(u16::from(*i), SlotKind::Int),
Instruction::Lstore(i) => store(u16::from(*i), SlotKind::Long),
Instruction::Fstore(i) => store(u16::from(*i), SlotKind::Float),
Instruction::Dstore(i) => store(u16::from(*i), SlotKind::Double),
Instruction::Astore(i) => store(u16::from(*i), SlotKind::Ref),
Instruction::Istore_w(i) => store(*i, SlotKind::Int),
Instruction::Lstore_w(i) => store(*i, SlotKind::Long),
Instruction::Fstore_w(i) => store(*i, SlotKind::Float),
Instruction::Dstore_w(i) => store(*i, SlotKind::Double),
Instruction::Astore_w(i) => store(*i, SlotKind::Ref),
Instruction::Istore_0 => store(0, SlotKind::Int),
Instruction::Istore_1 => store(1, SlotKind::Int),
Instruction::Istore_2 => store(2, SlotKind::Int),
Instruction::Istore_3 => store(3, SlotKind::Int),
Instruction::Lstore_0 => store(0, SlotKind::Long),
Instruction::Lstore_1 => store(1, SlotKind::Long),
Instruction::Lstore_2 => store(2, SlotKind::Long),
Instruction::Lstore_3 => store(3, SlotKind::Long),
Instruction::Fstore_0 => store(0, SlotKind::Float),
Instruction::Fstore_1 => store(1, SlotKind::Float),
Instruction::Fstore_2 => store(2, SlotKind::Float),
Instruction::Fstore_3 => store(3, SlotKind::Float),
Instruction::Dstore_0 => store(0, SlotKind::Double),
Instruction::Dstore_1 => store(1, SlotKind::Double),
Instruction::Dstore_2 => store(2, SlotKind::Double),
Instruction::Dstore_3 => store(3, SlotKind::Double),
Instruction::Astore_0 => store(0, SlotKind::Ref),
Instruction::Astore_1 => store(1, SlotKind::Ref),
Instruction::Astore_2 => store(2, SlotKind::Ref),
Instruction::Astore_3 => store(3, SlotKind::Ref),
Instruction::Iinc(i, d) => Insn::Iinc {
index: u16::from(*i),
delta: i32::from(*d),
},
Instruction::Iinc_w(i, d) => Insn::Iinc {
index: *i,
delta: i32::from(*d),
},
Instruction::Baload => Insn::ArrayLoad(ArrayKind::Byte),
Instruction::Caload => Insn::ArrayLoad(ArrayKind::Char),
Instruction::Saload => Insn::ArrayLoad(ArrayKind::Short),
Instruction::Iaload => Insn::ArrayLoad(ArrayKind::Int),
Instruction::Laload => Insn::ArrayLoad(ArrayKind::Long),
Instruction::Faload => Insn::ArrayLoad(ArrayKind::Float),
Instruction::Daload => Insn::ArrayLoad(ArrayKind::Double),
Instruction::Aaload => Insn::ArrayLoad(ArrayKind::Reference),
Instruction::Bastore => Insn::ArrayStore(ArrayKind::Byte),
Instruction::Castore => Insn::ArrayStore(ArrayKind::Char),
Instruction::Sastore => Insn::ArrayStore(ArrayKind::Short),
Instruction::Iastore => Insn::ArrayStore(ArrayKind::Int),
Instruction::Lastore => Insn::ArrayStore(ArrayKind::Long),
Instruction::Fastore => Insn::ArrayStore(ArrayKind::Float),
Instruction::Dastore => Insn::ArrayStore(ArrayKind::Double),
Instruction::Aastore => Insn::ArrayStore(ArrayKind::Reference),
Instruction::Pop => Insn::Pop,
Instruction::Pop2 => Insn::Pop2,
Instruction::Dup => Insn::Dup,
Instruction::Dup_x1 => Insn::DupX1,
Instruction::Dup_x2 => Insn::DupX2,
Instruction::Dup2 => Insn::Dup2,
Instruction::Dup2_x1 => Insn::Dup2X1,
Instruction::Dup2_x2 => Insn::Dup2X2,
Instruction::Swap => Insn::Swap,
Instruction::Iadd => binary(Prim::Int, Arith::Add),
Instruction::Isub => binary(Prim::Int, Arith::Sub),
Instruction::Imul => binary(Prim::Int, Arith::Mul),
Instruction::Idiv => binary(Prim::Int, Arith::Div),
Instruction::Irem => binary(Prim::Int, Arith::Rem),
Instruction::Ineg => binary(Prim::Int, Arith::Neg),
Instruction::Iand => binary(Prim::Int, Arith::And),
Instruction::Ior => binary(Prim::Int, Arith::Or),
Instruction::Ixor => binary(Prim::Int, Arith::Xor),
Instruction::Ishl => binary(Prim::Int, Arith::Shl),
Instruction::Ishr => binary(Prim::Int, Arith::Shr),
Instruction::Iushr => binary(Prim::Int, Arith::Ushr),
Instruction::Ladd => binary(Prim::Long, Arith::Add),
Instruction::Lsub => binary(Prim::Long, Arith::Sub),
Instruction::Lmul => binary(Prim::Long, Arith::Mul),
Instruction::Ldiv => binary(Prim::Long, Arith::Div),
Instruction::Lrem => binary(Prim::Long, Arith::Rem),
Instruction::Lneg => binary(Prim::Long, Arith::Neg),
Instruction::Land => binary(Prim::Long, Arith::And),
Instruction::Lor => binary(Prim::Long, Arith::Or),
Instruction::Lxor => binary(Prim::Long, Arith::Xor),
Instruction::Lshl => binary(Prim::Long, Arith::Shl),
Instruction::Lshr => binary(Prim::Long, Arith::Shr),
Instruction::Lushr => binary(Prim::Long, Arith::Ushr),
Instruction::Fadd => binary(Prim::Float, Arith::Add),
Instruction::Fsub => binary(Prim::Float, Arith::Sub),
Instruction::Fmul => binary(Prim::Float, Arith::Mul),
Instruction::Fdiv => binary(Prim::Float, Arith::Div),
Instruction::Frem => binary(Prim::Float, Arith::Rem),
Instruction::Fneg => binary(Prim::Float, Arith::Neg),
Instruction::Dadd => binary(Prim::Double, Arith::Add),
Instruction::Dsub => binary(Prim::Double, Arith::Sub),
Instruction::Dmul => binary(Prim::Double, Arith::Mul),
Instruction::Ddiv => binary(Prim::Double, Arith::Div),
Instruction::Drem => binary(Prim::Double, Arith::Rem),
Instruction::Dneg => binary(Prim::Double, Arith::Neg),
Instruction::Lcmp => cmp(Prim::Long, false),
Instruction::Fcmpl => cmp(Prim::Float, false),
Instruction::Fcmpg => cmp(Prim::Float, true),
Instruction::Dcmpl => cmp(Prim::Double, false),
Instruction::Dcmpg => cmp(Prim::Double, true),
Instruction::I2l => Insn::Convert(Convert::I2L),
Instruction::I2f => Insn::Convert(Convert::I2F),
Instruction::I2d => Insn::Convert(Convert::I2D),
Instruction::I2b => Insn::Convert(Convert::I2B),
Instruction::I2c => Insn::Convert(Convert::I2C),
Instruction::I2s => Insn::Convert(Convert::I2S),
Instruction::L2i => Insn::Convert(Convert::L2I),
Instruction::L2f => Insn::Convert(Convert::L2F),
Instruction::L2d => Insn::Convert(Convert::L2D),
Instruction::F2i => Insn::Convert(Convert::F2I),
Instruction::F2l => Insn::Convert(Convert::F2L),
Instruction::F2d => Insn::Convert(Convert::F2D),
Instruction::D2i => Insn::Convert(Convert::D2I),
Instruction::D2l => Insn::Convert(Convert::D2L),
Instruction::D2f => Insn::Convert(Convert::D2F),
_ => return Ok(None),
}))
}
fn load(slot: u16, kind: SlotKind) -> Insn {
Insn::Load { slot, kind }
}
fn store(slot: u16, kind: SlotKind) -> Insn {
Insn::Store { slot, kind }
}
fn binary(prim: Prim, op: Arith) -> Insn {
Insn::Binary { prim, op }
}
fn cmp(prim: Prim, nan_great: bool) -> Insn {
Insn::Cmp { prim, nan_great }
}

View file

@ -0,0 +1,98 @@
//! Ветвления, `switch` и возвраты: абсолютные/относительные цели → метки.
use ristretto_classfile::attributes::{Instruction, LookupSwitch, TableSwitch};
use super::Resolver;
use crate::error::ClassfileError;
use crate::insn::{BranchCond, Cond, Insn, Label, Prim};
/// `Some` — инструкция покрыта, `None` — отдать следующим конвертерам.
pub(super) fn convert_flow(
instruction: &Instruction,
index: u16,
resolve: &Resolver<'_>,
) -> Result<Option<Insn>, ClassfileError> {
let rel = |delta: i32| -> Result<Label, ClassfileError> { abs(index, delta, resolve) };
Ok(Some(match instruction {
Instruction::Ifeq(t) => branch(BranchCond::Zero(Cond::Eq), *t, resolve)?,
Instruction::Ifne(t) => branch(BranchCond::Zero(Cond::Ne), *t, resolve)?,
Instruction::Iflt(t) => branch(BranchCond::Zero(Cond::Lt), *t, resolve)?,
Instruction::Ifge(t) => branch(BranchCond::Zero(Cond::Ge), *t, resolve)?,
Instruction::Ifgt(t) => branch(BranchCond::Zero(Cond::Gt), *t, resolve)?,
Instruction::Ifle(t) => branch(BranchCond::Zero(Cond::Le), *t, resolve)?,
Instruction::If_icmpeq(t) => branch(BranchCond::Int(Cond::Eq), *t, resolve)?,
Instruction::If_icmpne(t) => branch(BranchCond::Int(Cond::Ne), *t, resolve)?,
Instruction::If_icmplt(t) => branch(BranchCond::Int(Cond::Lt), *t, resolve)?,
Instruction::If_icmpge(t) => branch(BranchCond::Int(Cond::Ge), *t, resolve)?,
Instruction::If_icmpgt(t) => branch(BranchCond::Int(Cond::Gt), *t, resolve)?,
Instruction::If_icmple(t) => branch(BranchCond::Int(Cond::Le), *t, resolve)?,
Instruction::If_acmpeq(t) => branch(BranchCond::Ref { eq: true }, *t, resolve)?,
Instruction::If_acmpne(t) => branch(BranchCond::Ref { eq: false }, *t, resolve)?,
Instruction::Ifnull(t) => branch(BranchCond::Null { is_null: true }, *t, resolve)?,
Instruction::Ifnonnull(t) => branch(BranchCond::Null { is_null: false }, *t, resolve)?,
Instruction::Goto(t) => Insn::Goto(resolve(*t)?),
Instruction::Goto_w(t) => Insn::Goto(resolve(wide(*t)?)?),
Instruction::Jsr(t) => Insn::Jsr(resolve(*t)?),
Instruction::Jsr_w(t) => Insn::Jsr(resolve(wide(*t)?)?),
Instruction::Ret(i) => Insn::Ret(u16::from(*i)),
Instruction::Ret_w(i) => Insn::Ret(*i),
Instruction::Tableswitch(s) => switch_table(s, &rel)?,
Instruction::Lookupswitch(s) => switch_lookup(s, &rel)?,
Instruction::Return => Insn::Return(None),
Instruction::Ireturn => Insn::Return(Some(Prim::Int)),
Instruction::Lreturn => Insn::Return(Some(Prim::Long)),
Instruction::Freturn => Insn::Return(Some(Prim::Float)),
Instruction::Dreturn => Insn::Return(Some(Prim::Double)),
Instruction::Areturn => Insn::AReturn,
_ => return Ok(None),
}))
}
fn branch(cond: BranchCond, target: u16, resolve: &Resolver<'_>) -> Result<Insn, ClassfileError> {
Ok(Insn::Branch(cond, resolve(target)?))
}
/// Цель `switch` — номер инструкции: база плюс относительная дельта backend'а.
fn abs(index: u16, delta: i32, resolve: &Resolver<'_>) -> Result<Label, ClassfileError> {
let absolute = i32::from(index)
.checked_add(delta)
.ok_or_else(|| ClassfileError::Parse("переполнение смещения ветвления".into()))?;
let index = u16::try_from(absolute)
.map_err(|_| ClassfileError::Parse("смещение ветвления вне диапазона u16".into()))?;
resolve(index)
}
fn wide(target: i32) -> Result<u16, ClassfileError> {
u16::try_from(target)
.map_err(|_| ClassfileError::Parse("широкий переход вне диапазона u16".into()))
}
fn switch_table(
table: &TableSwitch,
rel: &dyn Fn(i32) -> Result<Label, ClassfileError>,
) -> Result<Insn, ClassfileError> {
Ok(Insn::TableSwitch {
default: rel(table.default)?,
low: table.low,
high: table.high,
targets: table
.offsets
.iter()
.map(|offset| rel(*offset))
.collect::<Result<_, _>>()?,
})
}
fn switch_lookup(
lookup: &LookupSwitch,
rel: &dyn Fn(i32) -> Result<Label, ClassfileError>,
) -> Result<Insn, ClassfileError> {
Ok(Insn::LookupSwitch {
default: rel(lookup.default)?,
pairs: lookup
.pairs
.iter()
.map(|(key, offset)| Ok((*key, rel(*offset)?)))
.collect::<Result<_, _>>()?,
})
}

View file

@ -0,0 +1,48 @@
//! `Instruction` backend'а → [`Insn`](crate::insn::Insn) нашего IR.
//!
//! Смещения ветвлений приходят из backend'а (абсолютные для `if*`/`goto`,
//! относительные от начала инструкции для `switch`), поэтому конвертация
//! принимает резолвер смещения в [`Label`](crate::insn::Label) — метки
//! назначает вызывающий код.
mod basic;
mod flow;
mod refs;
use ristretto_classfile::ConstantPool;
use ristretto_classfile::attributes::Instruction;
use crate::error::ClassfileError;
use crate::insn::{Insn, Label};
/// Резолвит абсолютное смещение байткода в метку.
pub(super) type Resolver<'a> = dyn Fn(u16) -> Result<Label, ClassfileError> + 'a;
/// Конвертировать одну инструкцию. `index` — её номер в теле метода (нужно `switch`).
pub(super) fn convert(
instruction: &Instruction,
index: u16,
pool: &ConstantPool<'_>,
resolve: &Resolver<'_>,
) -> Result<Insn, ClassfileError> {
if let Some(insn) = basic::convert_basic(instruction, pool)? {
return Ok(insn);
}
if let Some(insn) = flow::convert_flow(instruction, index, resolve)? {
return Ok(insn);
}
if let Some(insn) = refs::convert_ref(instruction, pool)? {
return Ok(insn);
}
match instruction {
Instruction::Breakpoint | Instruction::Impdep1 | Instruction::Impdep2 => Err(
ClassfileError::Parse("зарезервированный опкод в коде метода".into()),
),
Instruction::Wide => Err(ClassfileError::Parse(
"wide обязан быть развёрнут парсером backend'а".into(),
)),
other => Err(ClassfileError::Parse(format!(
"неизвестная инструкция backend'а: {other:?}"
))),
}
}

View file

@ -0,0 +1,102 @@
//! Поля, вызовы и создание объектов: индексы пула → строки IR.
use ristretto_classfile::ConstantPool;
use ristretto_classfile::attributes::{ArrayType, Instruction};
use super::super::pool::{class_name, dynamic_parts, expect_kind, is_interface, ref_parts};
use crate::error::ClassfileError;
use crate::insn::{ArrayKind, Insn, InvokeKind};
/// `Some` — инструкция покрыта, `None` — отдать следующим конвертерам.
pub(super) fn convert_ref(
instruction: &Instruction,
pool: &ConstantPool<'_>,
) -> Result<Option<Insn>, ClassfileError> {
Ok(Some(match instruction {
Instruction::Getstatic(i) => {
let (owner, name, descriptor) = ref_parts(pool, *i)?;
Insn::GetStatic {
owner,
name,
descriptor,
}
}
Instruction::Putstatic(i) => {
let (owner, name, descriptor) = ref_parts(pool, *i)?;
Insn::PutStatic {
owner,
name,
descriptor,
}
}
Instruction::Getfield(i) => {
let (owner, name, descriptor) = ref_parts(pool, *i)?;
Insn::GetField {
owner,
name,
descriptor,
}
}
Instruction::Putfield(i) => {
let (owner, name, descriptor) = ref_parts(pool, *i)?;
Insn::PutField {
owner,
name,
descriptor,
}
}
Instruction::Invokevirtual(i) => invoke(pool, *i, InvokeKind::Virtual)?,
Instruction::Invokespecial(i) => invoke(pool, *i, InvokeKind::Special)?,
Instruction::Invokestatic(i) => invoke(pool, *i, InvokeKind::Static)?,
Instruction::Invokeinterface(i, _) => invoke(pool, *i, InvokeKind::Interface)?,
Instruction::Invokedynamic(i) => dynamic(pool, *i)?,
Instruction::New(i) => Insn::New(class_name(pool, *i)?),
Instruction::Newarray(t) => Insn::NewArray(array_kind(t.clone())),
Instruction::Anewarray(i) => Insn::ANewArray(class_name(pool, *i)?),
Instruction::Multianewarray(i, dims) => Insn::MultiANewArray {
descriptor: class_name(pool, *i)?,
dims: *dims,
},
Instruction::Arraylength => Insn::ArrayLength,
Instruction::Athrow => Insn::AThrow,
Instruction::Checkcast(i) => Insn::CheckCast(class_name(pool, *i)?),
Instruction::Instanceof(i) => Insn::InstanceOf(class_name(pool, *i)?),
Instruction::Monitorenter => Insn::MonitorEnter,
Instruction::Monitorexit => Insn::MonitorExit,
_ => return Ok(None),
}))
}
fn invoke(pool: &ConstantPool<'_>, index: u16, kind: InvokeKind) -> Result<Insn, ClassfileError> {
expect_kind(pool, index, kind)?;
let (owner, name, descriptor) = ref_parts(pool, index)?;
Ok(Insn::Invoke {
kind,
owner,
name,
descriptor,
itf: is_interface(pool, index),
})
}
fn dynamic(pool: &ConstantPool<'_>, index: u16) -> Result<Insn, ClassfileError> {
let (name, descriptor, bootstrap) = dynamic_parts(pool, index)?;
Ok(Insn::InvokeDynamic {
name,
descriptor,
bootstrap,
})
}
fn array_kind(kind: ArrayType) -> ArrayKind {
match kind {
ArrayType::Boolean => ArrayKind::Boolean,
ArrayType::Char => ArrayKind::Char,
ArrayType::Float => ArrayKind::Float,
ArrayType::Double => ArrayKind::Double,
ArrayType::Byte => ArrayKind::Byte,
ArrayType::Short => ArrayKind::Short,
ArrayType::Int => ArrayKind::Int,
ArrayType::Long => ArrayKind::Long,
}
}

View file

@ -0,0 +1,12 @@
//! Backend `ristretto_classfile` → наш IR: пул, инструкции, тела методов.
//!
//! Пока ниоткуда не подключён (P1): `format` вернёт `Unimplemented`, пока не
//! будут готовы `parse`/`write` классового уровня поверх этих конвертеров.
//! Дерево уже разбито по ответственностям, чтобы подключение было одним
//! `mod parse` без рефакторинга.
pub(super) mod attrs;
pub(super) mod class;
pub(super) mod code;
pub(super) mod insns;
pub(super) mod pool;

View file

@ -0,0 +1,184 @@
//! Разрешение индексов constant pool в строки нашего IR.
use ristretto_classfile::{Constant, ConstantPool, ReferenceKind};
use crate::error::ClassfileError;
use crate::insn::{Const, InvokeKind};
/// Имя класса по `CONSTANT_Class`.
pub(super) fn class_name(pool: &ConstantPool<'_>, index: u16) -> Result<String, ClassfileError> {
let name = pool
.try_get_class(index)
.map_err(|e| ClassfileError::Parse(e.to_string()))?;
Ok(name.to_string())
}
/// `(owner, name, descriptor)` для `CONSTANT_Fieldref`/`_Methodref`.
pub(super) fn ref_parts(
pool: &ConstantPool<'_>,
index: u16,
) -> Result<(String, String, String), ClassfileError> {
let constant = pool
.try_get(index)
.map_err(|e| ClassfileError::Parse(e.to_string()))?;
let (class_index, name_and_type_index) = match constant {
Constant::FieldRef {
class_index,
name_and_type_index,
}
| Constant::MethodRef {
class_index,
name_and_type_index,
}
| Constant::InterfaceMethodRef {
class_index,
name_and_type_index,
} => (*class_index, *name_and_type_index),
other => {
return Err(ClassfileError::Parse(format!(
"ожидалась ссылка на поле/метод, найден {other:?}"
)));
}
};
let owner = class_name(pool, class_index)?;
let (name, descriptor) = name_and_type(pool, name_and_type_index)?;
Ok((owner, name, descriptor))
}
/// `(name, descriptor)` для `CONSTANT_NameAndType`.
pub(super) fn name_and_type(
pool: &ConstantPool<'_>,
index: u16,
) -> Result<(String, String), ClassfileError> {
let (name_index, descriptor_index) = pool
.try_get_name_and_type(index)
.map_err(|e| ClassfileError::Parse(e.to_string()))?;
let name = utf8(pool, *name_index)?;
let descriptor = utf8(pool, *descriptor_index)?;
Ok((name, descriptor))
}
/// Строка из `CONSTANT_Utf8`.
pub(super) fn utf8(pool: &ConstantPool<'_>, index: u16) -> Result<String, ClassfileError> {
let value = pool
.try_get_utf8(index)
.map_err(|e| ClassfileError::Parse(e.to_string()))?;
Ok(value.to_string())
}
/// Константа для `ldc`/`ldc_w`/`ldc2_w`.
pub(super) fn const_from(pool: &ConstantPool<'_>, index: u16) -> Result<Const, ClassfileError> {
let constant = pool
.try_get(index)
.map_err(|e| ClassfileError::Parse(e.to_string()))?;
Ok(match constant {
Constant::Integer(v) => Const::Int(*v),
Constant::Float(v) => Const::Float(*v),
Constant::Long(v) => Const::Long(*v),
Constant::Double(v) => Const::Double(*v),
Constant::String(v) => Const::String(utf8(pool, *v)?),
Constant::Class(v) => Const::Class(utf8(pool, *v)?),
Constant::MethodType(v) => Const::MethodType(utf8(pool, *v)?),
Constant::MethodHandle {
reference_kind,
reference_index,
} => Const::MethodHandle {
kind: kind_code(reference_kind),
owner: ref_parts(pool, *reference_index)?.0,
name: ref_parts(pool, *reference_index)?.1,
descriptor: ref_parts(pool, *reference_index)?.2,
itf: matches!(
pool.try_get(*reference_index),
Ok(Constant::InterfaceMethodRef { .. })
),
},
Constant::Dynamic {
bootstrap_method_attr_index,
name_and_type_index,
} => {
let (name, descriptor) = name_and_type(pool, *name_and_type_index)?;
Const::Dynamic {
name,
descriptor,
bootstrap: *bootstrap_method_attr_index,
}
}
other => {
return Err(ClassfileError::Parse(format!(
"константа {other:?} недопустима в ldc"
)));
}
})
}
/// `(name, descriptor, bootstrap)` для `CONSTANT_InvokeDynamic`.
pub(super) fn dynamic_parts(
pool: &ConstantPool<'_>,
index: u16,
) -> Result<(String, String, u16), ClassfileError> {
let constant = pool
.try_get(index)
.map_err(|e| ClassfileError::Parse(e.to_string()))?;
let (bootstrap, name_and_type_index) = match constant {
Constant::InvokeDynamic {
bootstrap_method_attr_index,
name_and_type_index,
} => (*bootstrap_method_attr_index, *name_and_type_index),
other => {
return Err(ClassfileError::Parse(format!(
"ожидалась InvokeDynamic, найден {other:?}"
)));
}
};
let (name, descriptor) = name_and_type(pool, name_and_type_index)?;
Ok((name, descriptor, bootstrap))
}
/// Вид вызова по индексу ссылки: интерфейсная или обычная.
pub(super) fn is_interface(pool: &ConstantPool<'_>, index: u16) -> bool {
matches!(pool.try_get(index), Ok(Constant::InterfaceMethodRef { .. }))
}
/// JVMS §5.4.3.5: номер `reference_kind` для `CONSTANT_MethodHandle`.
pub(super) fn kind_code(kind: &ReferenceKind) -> u8 {
match kind {
ReferenceKind::GetField => 1,
ReferenceKind::GetStatic => 2,
ReferenceKind::PutField => 3,
ReferenceKind::PutStatic => 4,
ReferenceKind::InvokeVirtual => 5,
ReferenceKind::InvokeStatic => 6,
ReferenceKind::InvokeSpecial => 7,
ReferenceKind::NewInvokeSpecial => 8,
ReferenceKind::InvokeInterface => 9,
}
}
/// Проверка, что `Invoke` действительно ссылается на ожидаемый вид.
///
/// JVM принимает `Methodref` и `InterfaceMethodref` для всех видов вызова,
/// кроме `invokeinterface` (там только интерфейсная); строгость сверх JVM
/// ломает чтение реальных классов (статические методы интерфейсов).
pub(super) fn expect_kind(
pool: &ConstantPool<'_>,
index: u16,
kind: InvokeKind,
) -> Result<(), ClassfileError> {
let constant = pool
.try_get(index)
.map_err(|e| ClassfileError::Parse(e.to_string()))?;
let expected = match kind {
InvokeKind::Interface => matches!(constant, Constant::InterfaceMethodRef { .. }),
_ => matches!(
constant,
Constant::MethodRef { .. } | Constant::InterfaceMethodRef { .. }
),
};
if expected {
Ok(())
} else {
Err(ClassfileError::Parse(format!(
"вызов {kind:?} указывает на несовместимую ссылку"
)))
}
}

View file

@ -0,0 +1,250 @@
//! Атрибуты нашего IR → атрибуты backend'а.
//!
//! Имена атрибутов (`SourceFile`, …) интернируются в пул здесь же — backend
//! хранит только `name_index`, строка восстанавливается при чтении.
use ristretto_classfile::MethodAccessFlags;
use ristretto_classfile::attributes::{
Attribute as RtAttr, BootstrapMethod as RtBsm, InnerClass as RtInner, LineNumber as RtLine,
LocalVariableTable as RtLocal, LocalVariableTypeTable as RtLocalType,
MethodParameter as RtParam, NestedClassAccessFlags, Record as RtRecord,
};
use super::pool::Pool;
use crate::error::ClassfileError;
use crate::insn::{Const, Label};
use crate::model::{
Attribute, BootstrapMethod, InnerClass, LineNumber, LocalVariable, LocalVariableType,
MethodParameter,
};
/// Позиции таблиц кода: метки → байты текущего layout.
pub(super) struct Positions<'a> {
pub resolve: &'a dyn Fn(Label) -> Result<u32, ClassfileError>,
pub byte_of: &'a dyn Fn(u32) -> Result<u16, ClassfileError>,
}
/// `LineNumberTable`: backend хранит номера инструкций и маппит сам.
pub(super) fn line_number_table(
lines: &[LineNumber],
pool: &mut Pool,
positions: &Positions<'_>,
) -> Result<RtAttr, ClassfileError> {
Ok(RtAttr::LineNumberTable {
name_index: pool.utf8("LineNumberTable")?,
line_numbers: lines
.iter()
.map(|line| {
let index = (positions.resolve)(line.start)?;
Ok(RtLine {
start_pc: u16::try_from(index).map_err(|_| {
ClassfileError::Write(format!("номер инструкции {index} вне u16"))
})?,
line_number: line.line,
})
})
.collect::<Result<_, ClassfileError>>()?,
})
}
/// `LocalVariableTable`/`TypeTable` с пересчётом диапазона.
pub(super) fn local_table(
vars: &[LocalVariable],
pool: &mut Pool,
positions: &Positions<'_>,
) -> Result<RtAttr, ClassfileError> {
Ok(RtAttr::LocalVariableTable {
name_index: pool.utf8("LocalVariableTable")?,
variables: vars
.iter()
.map(|var| {
let (start_pc, length) = range(var.start, var.end, positions)?;
Ok(RtLocal {
start_pc,
length,
name_index: pool.utf8(&var.name)?,
descriptor_index: pool.utf8(&var.descriptor)?,
index: var.index,
})
})
.collect::<Result<_, ClassfileError>>()?,
})
}
/// Та же таблица для типов (`signature` вместо дескриптора).
pub(super) fn local_type_table(
vars: &[LocalVariableType],
pool: &mut Pool,
positions: &Positions<'_>,
) -> Result<RtAttr, ClassfileError> {
Ok(RtAttr::LocalVariableTypeTable {
name_index: pool.utf8("LocalVariableTypeTable")?,
variable_types: vars
.iter()
.map(|var| {
let (start_pc, length) = range(var.start, var.end, positions)?;
Ok(RtLocalType {
start_pc,
length,
name_index: pool.utf8(&var.name)?,
signature_index: pool.utf8(&var.signature)?,
index: var.index,
})
})
.collect::<Result<_, ClassfileError>>()?,
})
}
fn range(
start: Label,
end: Label,
positions: &Positions<'_>,
) -> Result<(u16, u16), ClassfileError> {
let first = (positions.byte_of)((positions.resolve)(start)?)?;
let last = (positions.byte_of)((positions.resolve)(end)?)?;
let length = last
.checked_sub(first)
.ok_or_else(|| ClassfileError::Write("конец раньше начала".into()))?;
Ok((first, length))
}
/// Один наш атрибут → атрибут backend'а.
pub(super) fn convert(attr: &Attribute, pool: &mut Pool) -> Result<RtAttr, ClassfileError> {
Ok(match attr {
Attribute::SourceFile(name) => RtAttr::SourceFile {
name_index: pool.utf8("SourceFile")?,
source_file_index: pool.utf8(name)?,
},
Attribute::Signature(value) => RtAttr::Signature {
name_index: pool.utf8("Signature")?,
signature_index: pool.utf8(value)?,
},
Attribute::Exceptions(names) => RtAttr::Exceptions {
name_index: pool.utf8("Exceptions")?,
exception_indexes: names
.iter()
.map(|name| pool.class(name))
.collect::<Result<_, _>>()?,
},
Attribute::Deprecated => RtAttr::Deprecated {
name_index: pool.utf8("Deprecated")?,
},
Attribute::Synthetic => RtAttr::Synthetic {
name_index: pool.utf8("Synthetic")?,
},
Attribute::InnerClasses(classes) => RtAttr::InnerClasses {
name_index: pool.utf8("InnerClasses")?,
classes: classes
.iter()
.map(|class| inner(class, pool))
.collect::<Result<_, _>>()?,
},
Attribute::ConstantValue(value) => RtAttr::ConstantValue {
name_index: pool.utf8("ConstantValue")?,
constant_value_index: pool.constant(value)?,
},
Attribute::BootstrapMethods(methods) => RtAttr::BootstrapMethods {
name_index: pool.utf8("BootstrapMethods")?,
methods: methods
.iter()
.map(|method| bootstrap(method, pool))
.collect::<Result<_, _>>()?,
},
Attribute::EnclosingMethod { class, method } => RtAttr::EnclosingMethod {
name_index: pool.utf8("EnclosingMethod")?,
class_index: pool.class(class)?,
method_index: match method {
None => 0,
Some((name, descriptor)) => pool.name_and_type(name, descriptor)?,
},
},
Attribute::MethodParameters(params) => RtAttr::MethodParameters {
name_index: pool.utf8("MethodParameters")?,
parameters: params
.iter()
.map(|param| method_param(param, pool))
.collect::<Result<_, _>>()?,
},
Attribute::Raw { name, data } => RtAttr::Unknown {
name_index: pool.utf8(name)?,
info: data.clone(),
},
Attribute::LineNumberTable(_)
| Attribute::LocalVariableTable(_)
| Attribute::LocalVariableTypeTable(_) => {
return Err(ClassfileError::Write(
"таблицы кода пишет вызывающий код метода".into(),
));
}
Attribute::Record(records) => RtAttr::Record {
name_index: pool.utf8("Record")?,
records: records
.iter()
.map(|record| {
Ok(RtRecord {
name_index: pool.utf8(&record.name)?,
descriptor_index: pool.utf8(&record.descriptor)?,
attributes: record
.attributes
.iter()
.map(|attr| convert(attr, pool))
.collect::<Result<_, _>>()?,
})
})
.collect::<Result<_, ClassfileError>>()?,
},
})
}
fn inner(class: &InnerClass, pool: &mut Pool) -> Result<RtInner, ClassfileError> {
Ok(RtInner {
class_info_index: pool.class(&class.inner)?,
outer_class_info_index: match &class.outer {
None => 0,
Some(name) => pool.class(name)?,
},
name_index: match &class.inner_name {
None => 0,
Some(name) => pool.utf8(name)?,
},
access_flags: NestedClassAccessFlags::from_bits_truncate(class.access_flags),
})
}
fn bootstrap(method: &BootstrapMethod, pool: &mut Pool) -> Result<RtBsm, ClassfileError> {
Ok(RtBsm {
bootstrap_method_ref: pool.method_handle(
method.kind,
&method.owner,
&method.name,
&method.descriptor,
method.itf,
)?,
arguments: method
.args
.iter()
.map(|arg| bootstrap_arg(arg, pool))
.collect::<Result<_, _>>()?,
})
}
/// Аргумент bootstrap-метода: те же константы, что в `ldc`, плюс `Dynamic`.
fn bootstrap_arg(arg: &Const, pool: &mut Pool) -> Result<u16, ClassfileError> {
match arg {
Const::Dynamic {
name,
descriptor,
bootstrap,
} => pool.dynamic(*bootstrap, name, descriptor),
_ => pool.constant(arg),
}
}
fn method_param(param: &MethodParameter, pool: &mut Pool) -> Result<RtParam, ClassfileError> {
Ok(RtParam {
name_index: match &param.name {
None => 0,
Some(name) => pool.utf8(name)?,
},
access_flags: MethodAccessFlags::from_bits_truncate(param.access_flags),
})
}

View file

@ -0,0 +1,111 @@
//! Кадры `StackMapTable` при записи: вариант в вариант, дельты как есть.
//!
//! Наш IR хранит кадры в номерах инструкций (как backend после чтения) —
//! backend сам пересчитает их в байты при `to_bytes`.
use ristretto_classfile::attributes::{StackFrame as RtFrame, VerificationType as RtType};
use super::super::pool::Pool;
use crate::error::ClassfileError;
use crate::format::{StackMapFrame, VerificationType};
/// Кадры IR → кадры backend'а (индексные дельты — как есть).
pub(super) fn convert_frames(
frames: &[StackMapFrame],
pool: &mut Pool,
) -> Result<Vec<RtFrame>, ClassfileError> {
frames
.iter()
.map(|frame| build_frame(frame, pool))
.collect()
}
fn build_frame(frame: &StackMapFrame, pool: &mut Pool) -> Result<RtFrame, ClassfileError> {
let delta = frame_delta(frame);
let offset = u16::try_from(delta)
.map_err(|_| ClassfileError::Write(format!("дельта кадра {delta} вне u16")))?;
Ok(match frame {
StackMapFrame::Same { .. } => {
if delta <= 63 {
RtFrame::SameFrame {
frame_type: delta as u8,
}
} else {
RtFrame::SameFrameExtended {
frame_type: 251,
offset_delta: offset,
}
}
}
StackMapFrame::SameLocals { local, .. } => {
let stack = vec![vtype(local, pool)?];
if delta <= 63 {
RtFrame::SameLocals1StackItemFrame {
frame_type: 64 + delta as u8,
stack,
}
} else {
RtFrame::SameLocals1StackItemFrameExtended {
frame_type: 247,
offset_delta: offset,
stack,
}
}
}
StackMapFrame::Chop { count, .. } => RtFrame::ChopFrame {
frame_type: 251u8.wrapping_sub(*count),
offset_delta: offset,
},
StackMapFrame::Append { locals, .. } => {
let count = u8::try_from(locals.len()).map_err(|_| {
ClassfileError::Write(format!("append-кадр на {} local'ов", locals.len()))
})?;
if !(1..=3).contains(&count) {
return Err(ClassfileError::Write(format!(
"append-кадр на {count} local'ов (нужно 1–3)"
)));
}
RtFrame::AppendFrame {
frame_type: 251 + count,
offset_delta: offset,
locals: types(locals, pool)?,
}
}
StackMapFrame::Full { locals, stack, .. } => RtFrame::FullFrame {
frame_type: 255,
offset_delta: offset,
locals: types(locals, pool)?,
stack: types(stack, pool)?,
},
})
}
fn frame_delta(frame: &StackMapFrame) -> u32 {
match *frame {
StackMapFrame::Same { offset_delta } => offset_delta,
StackMapFrame::SameLocals { offset_delta, .. } => offset_delta,
StackMapFrame::Chop { offset_delta, .. } => offset_delta,
StackMapFrame::Append { offset_delta, .. } => offset_delta,
StackMapFrame::Full { offset_delta, .. } => offset_delta,
}
}
fn types(values: &[VerificationType], pool: &mut Pool) -> Result<Vec<RtType>, ClassfileError> {
values.iter().map(|value| vtype(value, pool)).collect()
}
fn vtype(value: &VerificationType, pool: &mut Pool) -> Result<RtType, ClassfileError> {
Ok(match value {
VerificationType::Top => RtType::Top,
VerificationType::Integer => RtType::Integer,
VerificationType::Float => RtType::Float,
VerificationType::Double => RtType::Double,
VerificationType::Long => RtType::Long,
VerificationType::Null => RtType::Null,
VerificationType::UninitializedThis => RtType::UninitializedThis,
VerificationType::Object(name) => RtType::Object {
cpool_index: pool.class(name)?,
},
VerificationType::Uninitialized(offset) => RtType::Uninitialized { offset: *offset },
})
}

View file

@ -0,0 +1,197 @@
//! Наш [`Insn`](crate::insn::Insn) → `Instruction` backend'а.
//!
//! Обратная сторона `super::super::parse::insns`: метки резолвятся в
//! абсолютные номера инструкций (`switch` — в дельты от себя), константы
//! и ссылки уходят в пул через [`Pool`](super::super::pool::Pool).
use indexmap::IndexMap;
use ristretto_classfile::attributes::{Instruction as RtInsn, LookupSwitch, TableSwitch};
use super::super::pool::Pool;
use super::ops::{array_load, array_store, array_type, binary, cmp, convert_op, int_const, invoke};
use super::{branch, delta, fit_index, ret_op};
use crate::error::ClassfileError;
use crate::insn::{Insn, Label, SlotKind};
/// Одна инструкция IR → инструкция backend'а (`None` — маркер метки).
///
/// `index` — номер инструкции в методе (нужен `switch`-дельтам).
pub(crate) fn convert(
insn: &Insn,
index: u32,
pool: &mut Pool,
resolve: &impl Fn(Label) -> Result<u32, ClassfileError>,
) -> Result<Option<RtInsn>, ClassfileError> {
Ok(Some(match insn {
Insn::Mark(_) => return Ok(None),
Insn::Nop => RtInsn::Nop,
Insn::AconstNull => RtInsn::Aconst_null,
Insn::Const(value) => int_const(value, pool)?,
Insn::Load { slot, kind } => load(*slot, *kind),
Insn::Store { slot, kind } => store(*slot, *kind),
Insn::Iinc { index, delta } => iinc(*index, *delta)?,
Insn::ArrayLoad(kind) => array_load(*kind),
Insn::ArrayStore(kind) => array_store(*kind),
Insn::Pop => RtInsn::Pop,
Insn::Pop2 => RtInsn::Pop2,
Insn::Dup => RtInsn::Dup,
Insn::DupX1 => RtInsn::Dup_x1,
Insn::DupX2 => RtInsn::Dup_x2,
Insn::Dup2 => RtInsn::Dup2,
Insn::Dup2X1 => RtInsn::Dup2_x1,
Insn::Dup2X2 => RtInsn::Dup2_x2,
Insn::Swap => RtInsn::Swap,
Insn::Binary { prim, op } => binary(*prim, *op)?,
Insn::Cmp { prim, nan_great } => cmp(*prim, *nan_great)?,
Insn::Convert(op) => convert_op(*op),
Insn::Branch(cond, target) => branch(*cond, resolve(*target)?)?,
Insn::Goto(target) => RtInsn::Goto(fit_index(resolve(*target)?)?),
Insn::Jsr(target) => RtInsn::Jsr(fit_index(resolve(*target)?)?),
Insn::Ret(slot) => ret(*slot),
Insn::TableSwitch {
default,
low,
high,
targets,
} => RtInsn::Tableswitch(Box::new(TableSwitch {
default: delta(index, resolve(*default)?)?,
low: *low,
high: *high,
offsets: targets
.iter()
.map(|target| delta(index, resolve(*target)?))
.collect::<Result<_, _>>()?,
})),
Insn::LookupSwitch { default, pairs } => RtInsn::Lookupswitch(Box::new(LookupSwitch {
default: delta(index, resolve(*default)?)?,
pairs: pairs
.iter()
.map(|(key, target)| Ok((*key, delta(index, resolve(*target)?)?)))
.collect::<Result<IndexMap<_, _>, ClassfileError>>()?,
})),
Insn::Return(prim) => ret_op(*prim),
Insn::AReturn => RtInsn::Areturn,
Insn::GetStatic {
owner,
name,
descriptor,
} => RtInsn::Getstatic(pool.field_ref(owner, name, descriptor)?),
Insn::PutStatic {
owner,
name,
descriptor,
} => RtInsn::Putstatic(pool.field_ref(owner, name, descriptor)?),
Insn::GetField {
owner,
name,
descriptor,
} => RtInsn::Getfield(pool.field_ref(owner, name, descriptor)?),
Insn::PutField {
owner,
name,
descriptor,
} => RtInsn::Putfield(pool.field_ref(owner, name, descriptor)?),
Insn::Invoke {
kind,
owner,
name,
descriptor,
..
} => invoke(*kind, owner, name, descriptor, pool)?,
Insn::InvokeDynamic {
name,
descriptor,
bootstrap,
} => RtInsn::Invokedynamic(pool.invoke_dynamic(*bootstrap, name, descriptor)?),
Insn::New(name) => RtInsn::New(pool.class(name)?),
Insn::NewArray(kind) => RtInsn::Newarray(array_type(*kind)?),
Insn::ANewArray(name) => RtInsn::Anewarray(pool.class(name)?),
Insn::MultiANewArray { descriptor, dims } => {
RtInsn::Multianewarray(pool.class(descriptor)?, *dims)
}
Insn::ArrayLength => RtInsn::Arraylength,
Insn::AThrow => RtInsn::Athrow,
Insn::CheckCast(name) => RtInsn::Checkcast(pool.class(name)?),
Insn::InstanceOf(name) => RtInsn::Instanceof(pool.class(name)?),
Insn::MonitorEnter => RtInsn::Monitorenter,
Insn::MonitorExit => RtInsn::Monitorexit,
}))
}
fn load(slot: u16, kind: SlotKind) -> RtInsn {
match (kind, slot) {
(SlotKind::Int, 0) => RtInsn::Iload_0,
(SlotKind::Int, 1) => RtInsn::Iload_1,
(SlotKind::Int, 2) => RtInsn::Iload_2,
(SlotKind::Int, 3) => RtInsn::Iload_3,
(SlotKind::Long, 0) => RtInsn::Lload_0,
(SlotKind::Long, 1) => RtInsn::Lload_1,
(SlotKind::Long, 2) => RtInsn::Lload_2,
(SlotKind::Long, 3) => RtInsn::Lload_3,
(SlotKind::Float, 0) => RtInsn::Fload_0,
(SlotKind::Float, 1) => RtInsn::Fload_1,
(SlotKind::Float, 2) => RtInsn::Fload_2,
(SlotKind::Float, 3) => RtInsn::Fload_3,
(SlotKind::Double, 0) => RtInsn::Dload_0,
(SlotKind::Double, 1) => RtInsn::Dload_1,
(SlotKind::Double, 2) => RtInsn::Dload_2,
(SlotKind::Double, 3) => RtInsn::Dload_3,
(SlotKind::Ref, 0) => RtInsn::Aload_0,
(SlotKind::Ref, 1) => RtInsn::Aload_1,
(SlotKind::Ref, 2) => RtInsn::Aload_2,
(SlotKind::Ref, 3) => RtInsn::Aload_3,
(SlotKind::Int, s) => narrow(s, RtInsn::Iload, RtInsn::Iload_w),
(SlotKind::Long, s) => narrow(s, RtInsn::Lload, RtInsn::Lload_w),
(SlotKind::Float, s) => narrow(s, RtInsn::Fload, RtInsn::Fload_w),
(SlotKind::Double, s) => narrow(s, RtInsn::Dload, RtInsn::Dload_w),
(SlotKind::Ref, s) => narrow(s, RtInsn::Aload, RtInsn::Aload_w),
}
}
fn store(slot: u16, kind: SlotKind) -> RtInsn {
match (kind, slot) {
(SlotKind::Int, 0) => RtInsn::Istore_0,
(SlotKind::Int, 1) => RtInsn::Istore_1,
(SlotKind::Int, 2) => RtInsn::Istore_2,
(SlotKind::Int, 3) => RtInsn::Istore_3,
(SlotKind::Long, 0) => RtInsn::Lstore_0,
(SlotKind::Long, 1) => RtInsn::Lstore_1,
(SlotKind::Long, 2) => RtInsn::Lstore_2,
(SlotKind::Long, 3) => RtInsn::Lstore_3,
(SlotKind::Float, 0) => RtInsn::Fstore_0,
(SlotKind::Float, 1) => RtInsn::Fstore_1,
(SlotKind::Float, 2) => RtInsn::Fstore_2,
(SlotKind::Float, 3) => RtInsn::Fstore_3,
(SlotKind::Double, 0) => RtInsn::Dstore_0,
(SlotKind::Double, 1) => RtInsn::Dstore_1,
(SlotKind::Double, 2) => RtInsn::Dstore_2,
(SlotKind::Double, 3) => RtInsn::Dstore_3,
(SlotKind::Ref, 0) => RtInsn::Astore_0,
(SlotKind::Ref, 1) => RtInsn::Astore_1,
(SlotKind::Ref, 2) => RtInsn::Astore_2,
(SlotKind::Ref, 3) => RtInsn::Astore_3,
(SlotKind::Int, s) => narrow(s, RtInsn::Istore, RtInsn::Istore_w),
(SlotKind::Long, s) => narrow(s, RtInsn::Lstore, RtInsn::Lstore_w),
(SlotKind::Float, s) => narrow(s, RtInsn::Fstore, RtInsn::Fstore_w),
(SlotKind::Double, s) => narrow(s, RtInsn::Dstore, RtInsn::Dstore_w),
(SlotKind::Ref, s) => narrow(s, RtInsn::Astore, RtInsn::Astore_w),
}
}
/// Короткая форма при слоте ≤255, иначе `wide`-форма.
fn narrow(slot: u16, short: fn(u8) -> RtInsn, wide: fn(u16) -> RtInsn) -> RtInsn {
u8::try_from(slot).map(short).unwrap_or_else(|_| wide(slot))
}
fn ret(slot: u16) -> RtInsn {
narrow(slot, RtInsn::Ret, RtInsn::Ret_w)
}
fn iinc(index: u16, delta: i32) -> Result<RtInsn, ClassfileError> {
if let (Ok(i), Ok(d)) = (u8::try_from(index), i8::try_from(delta)) {
return Ok(RtInsn::Iinc(i, d));
}
let wide_delta = i16::try_from(delta)
.map_err(|_| ClassfileError::Write(format!("приращение iinc {delta} не влезает в i16")))?;
Ok(RtInsn::Iinc_w(index, wide_delta))
}

View file

@ -0,0 +1,231 @@
//! Тело метода нашего IR → атрибут `Code` backend'а.
//!
//! Метки резолвятся в номера инструкций один раз здесь; дальше backend сам
//! пересчитывает номера в байтовые смещения при `to_bytes`.
mod frames;
pub(crate) mod insns;
mod ops;
use std::collections::BTreeMap;
use std::io::Cursor;
use ristretto_classfile::attributes::{
Attribute as RtAttr, ExceptionTableEntry as RtEntry, Instruction as RtInsn,
};
use super::attrs;
use super::pool::Pool;
use crate::error::ClassfileError;
use crate::insn::{BranchCond, Cond, Insn, Label, Prim};
use crate::model::{Attribute, Code, ExceptionHandler};
use frames::convert_frames;
/// Тело метода → готовый атрибут `Code` (с логическими номерами инструкций).
pub(super) fn convert_code(code: &Code, pool: &mut Pool) -> Result<RtAttr, ClassfileError> {
let (labels, bare) = split_marks(&code.insns)?;
let resolve = |label: Label| -> Result<u32, ClassfileError> {
labels
.get(&label)
.copied()
.ok_or_else(|| ClassfileError::Write(format!("нет метки {label}")))
};
let mut rt_insns = Vec::with_capacity(bare.len());
for (index, insn) in bare.iter().enumerate() {
let index = u32::try_from(index)
.map_err(|_| ClassfileError::Write("в методе больше 4 млрд инструкций".into()))?;
if let Some(rt) = insns::convert(insn, index, pool, &resolve)? {
rt_insns.push(rt);
}
}
let exception_table = code
.exception_table
.iter()
.map(|handler| entry(handler, &resolve, pool))
.collect::<Result<_, _>>()?;
let to_byte = measure(&rt_insns)?;
let byte_of = |index: u32| -> Result<u16, ClassfileError> {
to_byte
.get(&index)
.copied()
.ok_or_else(|| ClassfileError::Write(format!("нет байта для инструкции {index}")))
};
let positions = attrs::Positions {
resolve: &resolve,
byte_of: &byte_of,
};
let frames = convert_frames(&code.frames, pool)?;
let mut attributes = Vec::with_capacity(code.attributes.len() + 1);
for attr in &code.attributes {
attributes.push(match attr {
// Backend хранит номера строк в индексах и маппит сам.
Attribute::LineNumberTable(lines) => attrs::line_number_table(lines, pool, &positions)?,
Attribute::LocalVariableTable(vars) => attrs::local_table(vars, pool, &positions)?,
Attribute::LocalVariableTypeTable(vars) => {
attrs::local_type_table(vars, pool, &positions)?
}
other => attrs::convert(other, pool)?,
});
}
if !frames.is_empty() {
attributes.push(RtAttr::StackMapTable {
name_index: pool.utf8("StackMapTable")?,
frames,
});
}
Ok(RtAttr::Code {
name_index: pool.utf8("Code")?,
max_stack: code.max_stack,
max_locals: code.max_locals,
code: rt_insns,
exception_table,
attributes,
})
}
/// Метки → номера инструкций; сами маркеры из потока убираются.
fn split_marks(insns: &[Insn]) -> Result<(BTreeMap<Label, u32>, Vec<&Insn>), ClassfileError> {
let mut labels = BTreeMap::new();
let mut bare = Vec::with_capacity(insns.len());
for insn in insns {
match insn {
Insn::Mark(label) => {
let index = u32::try_from(bare.len()).map_err(|_| {
ClassfileError::Write("в методе больше 4 млрд инструкций".into())
})?;
if let Some(prev) = labels.insert(*label, index)
&& prev != index
{
return Err(ClassfileError::Write(format!(
"метка {label} объявлена дважды"
)));
}
}
_ => bare.push(insn),
}
}
Ok((labels, bare))
}
/// Номер инструкции → байтовое смещение (замер с заглушками целей).
///
/// В карту входит и конец кода (`len` → длина): концы диапазонов указывают
/// за последнюю инструкцию.
pub(crate) fn measure(insns: &[RtInsn]) -> Result<BTreeMap<u32, u16>, ClassfileError> {
let mut map = BTreeMap::new();
let mut cursor = Cursor::new(Vec::new());
for (index, insn) in insns.iter().enumerate() {
let position = tell(&cursor)?;
let index = u32::try_from(index)
.map_err(|_| ClassfileError::Write("в методе больше 4 млрд инструкций".into()))?;
map.insert(index, position);
let mut sizing = insn.clone();
smash_targets(&mut sizing, position);
sizing
.to_bytes(&mut cursor)
.map_err(|e| ClassfileError::Write(e.to_string()))?;
}
let end = u32::try_from(insns.len())
.map_err(|_| ClassfileError::Write("в методе больше 4 млрд инструкций".into()))?;
map.insert(end, tell(&cursor)?);
Ok(map)
}
/// Цели ветвлений не влияют на размер — подменяем текущей позицией для замера.
fn smash_targets(insn: &mut RtInsn, position: u16) {
match insn {
RtInsn::Ifeq(target)
| RtInsn::Ifne(target)
| RtInsn::Iflt(target)
| RtInsn::Ifge(target)
| RtInsn::Ifgt(target)
| RtInsn::Ifle(target)
| RtInsn::If_icmpeq(target)
| RtInsn::If_icmpne(target)
| RtInsn::If_icmplt(target)
| RtInsn::If_icmpge(target)
| RtInsn::If_icmpgt(target)
| RtInsn::If_icmple(target)
| RtInsn::If_acmpeq(target)
| RtInsn::If_acmpne(target)
| RtInsn::Goto(target)
| RtInsn::Jsr(target)
| RtInsn::Ifnull(target)
| RtInsn::Ifnonnull(target) => *target = position,
RtInsn::Goto_w(target) | RtInsn::Jsr_w(target) => *target = i32::from(position),
_ => {}
}
}
fn tell(cursor: &Cursor<Vec<u8>>) -> Result<u16, ClassfileError> {
u16::try_from(cursor.position())
.map_err(|_| ClassfileError::Write("тело метода длиннее 64 КБ".into()))
}
fn entry(
handler: &ExceptionHandler,
resolve: &impl Fn(Label) -> Result<u32, ClassfileError>,
pool: &mut Pool,
) -> Result<RtEntry, ClassfileError> {
Ok(RtEntry {
range_pc: fit_index(resolve(handler.start)?)?..fit_index(resolve(handler.end)?)?,
handler_pc: fit_index(resolve(handler.handler)?)?,
catch_type: match &handler.catch_type {
None => 0,
Some(name) => pool.class(name)?,
},
})
}
/// Условный переход IR → `if*` backend'а.
fn branch(cond: BranchCond, target: u32) -> Result<RtInsn, ClassfileError> {
let target = fit_index(target)?;
Ok(match cond {
BranchCond::Zero(Cond::Eq) => RtInsn::Ifeq(target),
BranchCond::Zero(Cond::Ne) => RtInsn::Ifne(target),
BranchCond::Zero(Cond::Lt) => RtInsn::Iflt(target),
BranchCond::Zero(Cond::Ge) => RtInsn::Ifge(target),
BranchCond::Zero(Cond::Gt) => RtInsn::Ifgt(target),
BranchCond::Zero(Cond::Le) => RtInsn::Ifle(target),
BranchCond::Int(Cond::Eq) => RtInsn::If_icmpeq(target),
BranchCond::Int(Cond::Ne) => RtInsn::If_icmpne(target),
BranchCond::Int(Cond::Lt) => RtInsn::If_icmplt(target),
BranchCond::Int(Cond::Ge) => RtInsn::If_icmpge(target),
BranchCond::Int(Cond::Gt) => RtInsn::If_icmpgt(target),
BranchCond::Int(Cond::Le) => RtInsn::If_icmple(target),
BranchCond::Ref { eq: true } => RtInsn::If_acmpeq(target),
BranchCond::Ref { eq: false } => RtInsn::If_acmpne(target),
BranchCond::Null { is_null: true } => RtInsn::Ifnull(target),
BranchCond::Null { is_null: false } => RtInsn::Ifnonnull(target),
})
}
fn ret_op(prim: Option<Prim>) -> RtInsn {
match prim {
None => RtInsn::Return,
Some(Prim::Int) => RtInsn::Ireturn,
Some(Prim::Long) => RtInsn::Lreturn,
Some(Prim::Float) => RtInsn::Freturn,
Some(Prim::Double) => RtInsn::Dreturn,
}
}
/// Номер инструкции обязан влезать в `u16` (метод < 64 КБ кода).
fn fit_index(index: u32) -> Result<u16, ClassfileError> {
u16::try_from(index)
.map_err(|_| ClassfileError::Write(format!("номер инструкции {index} вне u16")))
}
/// Дельта `switch`: цель минус позиция самого `switch`.
fn delta(index: u32, target: u32) -> Result<i32, ClassfileError> {
let (index, target) = (to_i32(index)?, to_i32(target)?);
target
.checked_sub(index)
.ok_or_else(|| ClassfileError::Write(format!("переполнение дельты {index}->{target}")))
}
fn to_i32(value: u32) -> Result<i32, ClassfileError> {
i32::try_from(value)
.map_err(|_| ClassfileError::Write(format!("номер инструкции {value} вне i32")))
}

View file

@ -0,0 +1,244 @@
//! Операнды: константы, арифметика, массивы, вызовы.
use ristretto_classfile::attributes::{ArrayType, Instruction as RtInsn};
use super::super::pool::Pool;
use crate::error::ClassfileError;
use crate::insn::{Arith, ArrayKind, Const, Convert, InvokeKind, Prim};
/// Константа: короткие формы (`iconst`, `bipush`) + `ldc_w`/`ldc2_w` через пул.
pub(super) fn int_const(value: &Const, pool: &mut Pool) -> Result<RtInsn, ClassfileError> {
Ok(match value {
Const::Int(v) => match *v {
-1 => RtInsn::Iconst_m1,
0 => RtInsn::Iconst_0,
1 => RtInsn::Iconst_1,
2 => RtInsn::Iconst_2,
3 => RtInsn::Iconst_3,
4 => RtInsn::Iconst_4,
5 => RtInsn::Iconst_5,
v if i8::try_from(v).is_ok() => RtInsn::Bipush(v as i8),
v if i16::try_from(v).is_ok() => RtInsn::Sipush(v as i16),
_ => RtInsn::Ldc_w(pool.constant(value)?),
},
Const::Long(0) => RtInsn::Lconst_0,
Const::Long(1) => RtInsn::Lconst_1,
Const::Long(_) => RtInsn::Ldc2_w(pool.constant(value)?),
Const::Float(v) if *v == 0.0 => RtInsn::Fconst_0,
Const::Float(v) if *v == 1.0 => RtInsn::Fconst_1,
Const::Float(v) if *v == 2.0 => RtInsn::Fconst_2,
Const::Float(_) => RtInsn::Ldc_w(pool.constant(value)?),
Const::Double(v) if *v == 0.0 => RtInsn::Dconst_0,
Const::Double(v) if *v == 1.0 => RtInsn::Dconst_1,
Const::Double(_) => RtInsn::Ldc2_w(pool.constant(value)?),
Const::Dynamic {
name,
descriptor,
bootstrap,
} => RtInsn::Ldc2_w(pool.dynamic(*bootstrap, name, descriptor)?),
_ => RtInsn::Ldc_w(pool.constant(value)?),
})
}
pub(super) fn array_load(kind: ArrayKind) -> RtInsn {
match kind {
ArrayKind::Boolean | ArrayKind::Byte => RtInsn::Baload,
ArrayKind::Char => RtInsn::Caload,
ArrayKind::Short => RtInsn::Saload,
ArrayKind::Int => RtInsn::Iaload,
ArrayKind::Long => RtInsn::Laload,
ArrayKind::Float => RtInsn::Faload,
ArrayKind::Double => RtInsn::Daload,
ArrayKind::Reference => RtInsn::Aaload,
}
}
pub(super) fn array_store(kind: ArrayKind) -> RtInsn {
match kind {
ArrayKind::Boolean | ArrayKind::Byte => RtInsn::Bastore,
ArrayKind::Char => RtInsn::Castore,
ArrayKind::Short => RtInsn::Sastore,
ArrayKind::Int => RtInsn::Iastore,
ArrayKind::Long => RtInsn::Lastore,
ArrayKind::Float => RtInsn::Fastore,
ArrayKind::Double => RtInsn::Dastore,
ArrayKind::Reference => RtInsn::Aastore,
}
}
pub(super) fn array_type(kind: ArrayKind) -> Result<ArrayType, ClassfileError> {
Ok(match kind {
ArrayKind::Boolean => ArrayType::Boolean,
ArrayKind::Char => ArrayType::Char,
ArrayKind::Float => ArrayType::Float,
ArrayKind::Double => ArrayType::Double,
ArrayKind::Byte => ArrayType::Byte,
ArrayKind::Short => ArrayType::Short,
ArrayKind::Int => ArrayType::Int,
ArrayKind::Long => ArrayType::Long,
ArrayKind::Reference => {
return Err(ClassfileError::Write(
"newarray не создаёт ссылочные массивы (нужен anewarray)".into(),
));
}
})
}
pub(super) fn binary(prim: Prim, op: Arith) -> Result<RtInsn, ClassfileError> {
use Arith::{Add, And, Div, Mul, Neg, Or, Rem, Shl, Shr, Sub, Ushr, Xor};
Ok(match (prim, op) {
(Prim::Int, Add) => RtInsn::Iadd,
(Prim::Int, Sub) => RtInsn::Isub,
(Prim::Int, Mul) => RtInsn::Imul,
(Prim::Int, Div) => RtInsn::Idiv,
(Prim::Int, Rem) => RtInsn::Irem,
(Prim::Int, Neg) => RtInsn::Ineg,
(Prim::Int, And) => RtInsn::Iand,
(Prim::Int, Or) => RtInsn::Ior,
(Prim::Int, Xor) => RtInsn::Ixor,
(Prim::Int, Shl) => RtInsn::Ishl,
(Prim::Int, Shr) => RtInsn::Ishr,
(Prim::Int, Ushr) => RtInsn::Iushr,
(Prim::Long, Add) => RtInsn::Ladd,
(Prim::Long, Sub) => RtInsn::Lsub,
(Prim::Long, Mul) => RtInsn::Lmul,
(Prim::Long, Div) => RtInsn::Ldiv,
(Prim::Long, Rem) => RtInsn::Lrem,
(Prim::Long, Neg) => RtInsn::Lneg,
(Prim::Long, And) => RtInsn::Land,
(Prim::Long, Or) => RtInsn::Lor,
(Prim::Long, Xor) => RtInsn::Lxor,
(Prim::Long, Shl) => RtInsn::Lshl,
(Prim::Long, Shr) => RtInsn::Lshr,
(Prim::Long, Ushr) => RtInsn::Lushr,
(Prim::Float, Add) => RtInsn::Fadd,
(Prim::Float, Sub) => RtInsn::Fsub,
(Prim::Float, Mul) => RtInsn::Fmul,
(Prim::Float, Div) => RtInsn::Fdiv,
(Prim::Float, Rem) => RtInsn::Frem,
(Prim::Float, Neg) => RtInsn::Fneg,
(Prim::Double, Add) => RtInsn::Dadd,
(Prim::Double, Sub) => RtInsn::Dsub,
(Prim::Double, Mul) => RtInsn::Dmul,
(Prim::Double, Div) => RtInsn::Ddiv,
(Prim::Double, Rem) => RtInsn::Drem,
(Prim::Double, Neg) => RtInsn::Dneg,
(prim, op) => {
return Err(ClassfileError::Write(format!(
"недопустимая комбинация {prim:?}/{op:?}"
)));
}
})
}
pub(super) fn cmp(prim: Prim, nan_great: bool) -> Result<RtInsn, ClassfileError> {
Ok(match (prim, nan_great) {
(Prim::Long, false) => RtInsn::Lcmp,
(Prim::Float, false) => RtInsn::Fcmpl,
(Prim::Float, true) => RtInsn::Fcmpg,
(Prim::Double, false) => RtInsn::Dcmpl,
(Prim::Double, true) => RtInsn::Dcmpg,
(prim, nan_great) => {
return Err(ClassfileError::Write(format!(
"недопустимое сравнение {prim:?}/{nan_great}"
)));
}
})
}
pub(super) fn convert_op(op: Convert) -> RtInsn {
match op {
Convert::I2L => RtInsn::I2l,
Convert::I2F => RtInsn::I2f,
Convert::I2D => RtInsn::I2d,
Convert::I2B => RtInsn::I2b,
Convert::I2C => RtInsn::I2c,
Convert::I2S => RtInsn::I2s,
Convert::L2I => RtInsn::L2i,
Convert::L2F => RtInsn::L2f,
Convert::L2D => RtInsn::L2d,
Convert::F2I => RtInsn::F2i,
Convert::F2L => RtInsn::F2l,
Convert::F2D => RtInsn::F2d,
Convert::D2I => RtInsn::D2i,
Convert::D2L => RtInsn::D2l,
Convert::D2F => RtInsn::D2f,
}
}
pub(super) fn invoke(
kind: InvokeKind,
owner: &str,
name: &str,
descriptor: &str,
pool: &mut Pool,
) -> Result<RtInsn, ClassfileError> {
Ok(match kind {
InvokeKind::Virtual => {
RtInsn::Invokevirtual(pool.method_ref(owner, name, descriptor, kind)?)
}
InvokeKind::Special => {
RtInsn::Invokespecial(pool.method_ref(owner, name, descriptor, kind)?)
}
InvokeKind::Static => RtInsn::Invokestatic(pool.method_ref(owner, name, descriptor, kind)?),
InvokeKind::Interface => {
let index = pool.method_ref(owner, name, descriptor, kind)?;
RtInsn::Invokeinterface(index, arg_count(descriptor)?)
}
})
}
/// Байт `count` для `invokeinterface`: слоты аргументов + `objectref`.
fn arg_count(descriptor: &str) -> Result<u8, ClassfileError> {
let params = descriptor
.split_once('(')
.and_then(|(_, rest)| rest.split_once(')'))
.map(|(params, _)| params)
.ok_or_else(|| ClassfileError::Write(format!("кривой дескриптор {descriptor}")))?;
let mut slots = 1u32;
let mut chars = params.chars();
while let Some(char) = chars.next() {
match char {
'B' | 'C' | 'F' | 'I' | 'S' | 'Z' => slots += 1,
'J' | 'D' => slots += 2,
'L' => {
if !skip_to(chars.by_ref(), ';') {
return Err(bad_descriptor(descriptor));
}
slots += 1;
}
'[' => {
while chars.as_str().starts_with('[') {
chars.next();
}
match chars.next() {
Some('L') => {
if !skip_to(chars.by_ref(), ';') {
return Err(bad_descriptor(descriptor));
}
}
Some(_) => {}
None => return Err(bad_descriptor(descriptor)),
}
slots += 1;
}
_ => return Err(bad_descriptor(descriptor)),
}
}
u8::try_from(slots)
.map_err(|_| ClassfileError::Write(format!("слишком много аргументов в {descriptor}")))
}
/// Пропустить символы до разделителя (включительно); `false` — строка кончилась.
fn skip_to(chars: &mut std::str::Chars<'_>, end: char) -> bool {
for char in chars {
if char == end {
return true;
}
}
false
}
fn bad_descriptor(descriptor: &str) -> ClassfileError {
ClassfileError::Write(format!("кривой дескриптор {descriptor}"))
}

View file

@ -0,0 +1,93 @@
//! Наш IR → байты `.class` через backend `ristretto_classfile`.
//!
//! Пул собирается с нуля детерминированным обходом; backend сам пересчитывает
//! номера инструкций в байтовые смещения и проверяет результат (`verify`).
mod attrs;
pub(crate) mod code;
pub(crate) mod pool;
use ristretto_classfile::{
ClassAccessFlags, ClassFile as RtClass, Field as RtField, FieldAccessFlags, FieldType,
Method as RtMethod, MethodAccessFlags, Version as RtVersion,
};
use crate::error::ClassfileError;
use crate::model::{ClassFile, Field, Method};
/// Класс нашего IR → байты `.class`.
pub(crate) fn write_class(class: &ClassFile) -> Result<Vec<u8>, ClassfileError> {
let mut pool = pool::Pool::new();
let version = RtVersion::from(class.version.major, class.version.minor)
.map_err(|e| ClassfileError::Write(e.to_string()))?;
let this_class = pool.class(&class.this_class)?;
let super_class = match &class.super_class {
None => 0,
Some(name) => pool.class(name)?,
};
let mut interfaces = Vec::with_capacity(class.interfaces.len());
for interface in &class.interfaces {
interfaces.push(pool.class(interface)?);
}
let mut fields = Vec::with_capacity(class.fields.len());
for field in &class.fields {
fields.push(convert_field(field, &mut pool)?);
}
let mut methods = Vec::with_capacity(class.methods.len());
for method in &class.methods {
methods.push(convert_method(method, &mut pool)?);
}
let mut attributes = Vec::with_capacity(class.attributes.len());
for attr in &class.attributes {
attributes.push(attrs::convert(attr, &mut pool)?);
}
let rt = RtClass {
version,
constant_pool: pool.finish(),
access_flags: ClassAccessFlags::from_bits_truncate(class.access_flags),
this_class,
super_class,
interfaces,
fields,
methods,
attributes,
code_source_url: None,
};
rt.verify()
.map_err(|e| ClassfileError::Write(format!("собранный класс не верифицируется: {e}")))?;
let mut bytes = Vec::new();
rt.to_bytes(&mut bytes)
.map_err(|e| ClassfileError::Write(e.to_string()))?;
Ok(bytes)
}
fn convert_field(field: &Field, pool: &mut pool::Pool) -> Result<RtField, ClassfileError> {
Ok(RtField {
access_flags: FieldAccessFlags::from_bits_truncate(field.access_flags),
name_index: pool.utf8(&field.name)?,
descriptor_index: pool.utf8(&field.descriptor)?,
field_type: FieldType::parse(&field.descriptor)
.map_err(|e| ClassfileError::Write(format!("кривой дескриптор поля: {e}")))?,
attributes: field
.attributes
.iter()
.map(|attr| attrs::convert(attr, pool))
.collect::<Result<_, _>>()?,
})
}
fn convert_method(method: &Method, pool: &mut pool::Pool) -> Result<RtMethod, ClassfileError> {
let mut attributes = Vec::with_capacity(method.attributes.len() + 1);
for attr in &method.attributes {
attributes.push(attrs::convert(attr, pool)?);
}
if let Some(code) = &method.code {
attributes.push(code::convert_code(code, pool)?);
}
Ok(RtMethod {
access_flags: MethodAccessFlags::from_bits_truncate(method.access_flags),
name_index: pool.utf8(&method.name)?,
descriptor_index: pool.utf8(&method.descriptor)?,
attributes,
})
}

View file

@ -0,0 +1,155 @@
//! Сборка constant pool при записи: строки нашего IR → индексы backend'а.
//!
//! Пул строится с нуля детерминированным обходом (классы, поля, методы,
//! атрибуты, код), поэтому одинаковый IR даёт одинаковые байты. Индексы
//! `Raw`-атрибутов при этом не переносятся — см. `super::attrs`.
use ristretto_classfile::{ConstantPool, ReferenceKind};
use crate::error::ClassfileError;
use crate::insn::{Const, InvokeKind};
/// Строитель пула; живёт только внутри одного `write`.
pub(crate) struct Pool {
pool: ConstantPool<'static>,
}
impl Pool {
pub(crate) fn new() -> Self {
Pool {
pool: ConstantPool::default(),
}
}
pub(super) fn utf8(&mut self, value: &str) -> Result<u16, ClassfileError> {
self.pool.add_utf8(value).map_err(write_err)
}
pub(super) fn name_and_type(
&mut self,
name: &str,
descriptor: &str,
) -> Result<u16, ClassfileError> {
self.pool
.add_name_and_type(name, descriptor)
.map_err(write_err)
}
pub(super) fn class(&mut self, name: &str) -> Result<u16, ClassfileError> {
self.pool.add_class(name).map_err(write_err)
}
pub(super) fn field_ref(
&mut self,
owner: &str,
name: &str,
descriptor: &str,
) -> Result<u16, ClassfileError> {
let owner = self.class(owner)?;
self.pool
.add_field_ref(owner, name, descriptor)
.map_err(write_err)
}
pub(super) fn method_ref(
&mut self,
owner: &str,
name: &str,
descriptor: &str,
kind: InvokeKind,
) -> Result<u16, ClassfileError> {
let owner = self.class(owner)?;
match kind {
InvokeKind::Interface => self
.pool
.add_interface_method_ref(owner, name, descriptor)
.map_err(write_err),
_ => self
.pool
.add_method_ref(owner, name, descriptor)
.map_err(write_err),
}
}
/// Константа для `ldc`/`ldc2_w`; `dynamic` запрещён (только через bootstrap).
pub(super) fn constant(&mut self, value: &Const) -> Result<u16, ClassfileError> {
match value {
Const::Int(v) => self.pool.add_integer(*v).map_err(write_err),
Const::Float(v) => self.pool.add_float(*v).map_err(write_err),
Const::Long(v) => self.pool.add_long(*v).map_err(write_err),
Const::Double(v) => self.pool.add_double(*v).map_err(write_err),
Const::String(v) => self.pool.add_string(v).map_err(write_err),
Const::Class(v) => self.class(v),
Const::MethodType(v) => self.pool.add_method_type(v).map_err(write_err),
Const::MethodHandle {
kind,
owner,
name,
descriptor,
itf,
} => self.method_handle(*kind, owner, name, descriptor, *itf),
Const::Dynamic { .. } => Err(ClassfileError::Write(
"CONSTANT_Dynamic пишется только через BootstrapMethods".into(),
)),
}
}
pub(super) fn method_handle(
&mut self,
kind: u8,
owner: &str,
name: &str,
descriptor: &str,
itf: bool,
) -> Result<u16, ClassfileError> {
let reference_kind = ReferenceKind::try_from(kind).map_err(|e| {
ClassfileError::Write(format!("некорректный reference_kind {kind}: {e}"))
})?;
let reference = if itf {
self.method_ref(owner, name, descriptor, InvokeKind::Interface)?
} else if matches!(
reference_kind,
ReferenceKind::GetField
| ReferenceKind::GetStatic
| ReferenceKind::PutField
| ReferenceKind::PutStatic
) {
self.field_ref(owner, name, descriptor)?
} else {
self.method_ref(owner, name, descriptor, InvokeKind::Static)?
};
self.pool
.add_method_handle(reference_kind, reference)
.map_err(write_err)
}
pub(super) fn invoke_dynamic(
&mut self,
bootstrap: u16,
name: &str,
descriptor: &str,
) -> Result<u16, ClassfileError> {
self.pool
.add_invoke_dynamic(bootstrap, name, descriptor)
.map_err(write_err)
}
pub(super) fn dynamic(
&mut self,
bootstrap: u16,
name: &str,
descriptor: &str,
) -> Result<u16, ClassfileError> {
self.pool
.add_dynamic(bootstrap, name, descriptor)
.map_err(write_err)
}
pub(super) fn finish(self) -> ConstantPool<'static> {
self.pool
}
}
fn write_err(error: ristretto_classfile::Error) -> ClassfileError {
ClassfileError::Write(error.to_string())
}

View file

@ -0,0 +1,136 @@
//! Типы операндов инструкций: константы, примитивы, условия, вызовы.
/// Идентификатор метки внутри одного метода.
pub type Label = u32;
/// Константа для `ldc`/`ldc_w`/`ldc2_w`.
#[derive(Debug, Clone, PartialEq)]
pub enum Const {
Int(i32),
Long(i64),
Float(f32),
Double(f64),
String(String),
/// Внутреннее имя класса (`java/lang/String`).
Class(String),
MethodType(String),
MethodHandle {
kind: u8,
owner: String,
name: String,
descriptor: String,
itf: bool,
},
/// `CONSTANT_Dynamic`: имя, дескриптор, индекс bootstrap-метода.
Dynamic {
name: String,
descriptor: String,
bootstrap: u16,
},
}
/// Размер операнда/результата арифметики.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Prim {
Int,
Long,
Float,
Double,
}
/// Арифметическая/битовая операция; тип берётся из [`Prim`].
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Arith {
Add,
Sub,
Mul,
Div,
Rem,
Neg,
And,
Or,
Xor,
Shl,
Shr,
Ushr,
}
/// Условие перехода.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Cond {
Eq,
Ne,
Lt,
Le,
Gt,
Ge,
}
/// Вид условного перехода (соответствует семействам `if*`).
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum BranchCond {
/// `if<cond>` относительно нуля.
Zero(Cond),
/// `if_icmp<cond>`.
Int(Cond),
/// `if_acmp<eq|ne>`.
Ref { eq: bool },
/// `ifnull` / `ifnonnull`.
Null { is_null: bool },
}
/// Вид вызова.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum InvokeKind {
Virtual,
Special,
Static,
Interface,
}
/// Конверсия типов (`i2l`, `f2b`, ... — JVMS §6.5.i2<b|c|s|l|f|d>).
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Convert {
I2L,
I2F,
I2D,
I2B,
I2C,
I2S,
L2I,
L2F,
L2D,
F2I,
F2L,
F2D,
D2I,
D2L,
D2F,
}
/// Тип элемента массива для `newarray`/`*aload`/`*astore`.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ArrayKind {
Boolean,
Char,
Float,
Double,
Byte,
Short,
Int,
Long,
Reference,
}
/// Тип слота локальной переменной для `*load*`/`*store*`.
///
/// Хранится явно: выбор опкода (`iload` vs `aload`) — подсказка верификатору,
/// потерять её при round-trip нельзя.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum SlotKind {
Int,
Long,
Float,
Double,
Ref,
}

View file

@ -0,0 +1,164 @@
//! Инструкции IR: смещения отсутствуют, ветвления адресуют [`Label`].
//!
//! Это принципиально: любой пасс, меняющий порядок/длину инструкций, не обязан
//! пересчитывать jump-офсеты — это делает writer один раз в конце пайплайна.
mod kinds;
pub use kinds::{
Arith, ArrayKind, BranchCond, Cond, Const, Convert, InvokeKind, Label, Prim, SlotKind,
};
/// Мнемоника одной инструкции.
#[derive(Debug, Clone, PartialEq)]
pub enum Insn {
/// Маркер метки. Ветвления указывают на [`Label`], writer вычисляет смещения.
Mark(Label),
Nop,
AconstNull,
Const(Const),
/// Загрузка локальной переменной (`iload`, `aload`, ...).
Load {
slot: u16,
kind: SlotKind,
},
/// Сохранение локальной переменной.
Store {
slot: u16,
kind: SlotKind,
},
/// `iinc` — увеличение локальной переменной на константу.
Iinc {
index: u16,
delta: i32,
},
/// Загрузка элемента массива.
ArrayLoad(ArrayKind),
/// Сохранение элемента массива.
ArrayStore(ArrayKind),
Pop,
Pop2,
Dup,
DupX1,
DupX2,
Dup2,
Dup2X1,
Dup2X2,
Swap,
/// `iadd`, `lmul`, `lneg`, ... — тип + операция.
Binary {
prim: Prim,
op: Arith,
},
/// Сравнение длин (`lcmp`) и плавающих (`fcmpl`/`fcmpg`).
Cmp {
prim: Prim,
nan_great: bool,
},
/// Конверсия типов (`i2l`, `d2f`, ...).
Convert(Convert),
Branch(BranchCond, Label),
Goto(Label),
/// `jsr` — вызов подпрограммы (только классы версии ≤ 50).
Jsr(Label),
/// `ret` — возврат из подпрограммы (`jsr`/`ret` парны).
Ret(u16),
TableSwitch {
default: Label,
low: i32,
high: i32,
targets: Vec<Label>,
},
LookupSwitch {
default: Label,
pairs: Vec<(i32, Label)>,
},
/// `return`/`ireturn`/...; `None` — пустой возврат.
Return(Option<Prim>),
/// `areturn` — возврат ссылки (у [`Prim`] нет ссылочного типа).
AReturn,
GetStatic {
owner: String,
name: String,
descriptor: String,
},
PutStatic {
owner: String,
name: String,
descriptor: String,
},
GetField {
owner: String,
name: String,
descriptor: String,
},
PutField {
owner: String,
name: String,
descriptor: String,
},
Invoke {
kind: InvokeKind,
owner: String,
name: String,
descriptor: String,
itf: bool,
},
InvokeDynamic {
name: String,
descriptor: String,
bootstrap: u16,
},
New(String),
NewArray(ArrayKind),
ANewArray(String),
/// `multianewarray` — массив любого измерения; `dims` — сколько измерений со стека.
MultiANewArray {
descriptor: String,
dims: u8,
},
ArrayLength,
AThrow,
CheckCast(String),
InstanceOf(String),
MonitorEnter,
MonitorExit,
}
impl Insn {
/// Метки, на которые ссылается инструкция (для построения CFG).
pub fn targets(&self) -> Vec<Label> {
match self {
Insn::Branch(_, label) | Insn::Goto(label) | Insn::Jsr(label) => vec![*label],
Insn::TableSwitch {
default,
targets,
low: _,
high: _,
} => {
let mut all = Vec::with_capacity(targets.len() + 1);
all.push(*default);
all.extend(targets.iter().copied());
all
}
Insn::LookupSwitch { default, pairs } => {
let mut all = Vec::with_capacity(pairs.len() + 1);
all.push(*default);
all.extend(pairs.iter().map(|(k, l)| {
let _ = k;
*l
}));
all
}
_ => Vec::new(),
}
}
/// Незакодированный переход (для вставки opaque-предикатов и перестройки CFG).
pub fn branch_target_mut(&mut self) -> Option<&mut Label> {
match self {
Insn::Branch(_, label) | Insn::Goto(label) => Some(label),
_ => None,
}
}
}

View file

@ -0,0 +1,29 @@
//! # noxis-classfile
//!
//! Собственный IR для классовых файлов JVM: разбор, представление, запись.
//!
//! Почему свой IR, а не работа напрямую с крейтом-парсером: пассам нужна
//! стабильная модель (метки вместо смещений, осознанный набор атрибутов),
//! а не API третьей стороны, который меняется между версиями.
//!
//! Статус: модель и билдер готовы, бинарный parse/write — этап P1
//! (см. `docs/stack.md`).
pub mod error;
pub mod format;
pub mod insn;
pub mod model;
pub use error::ClassfileError;
pub use format::{
Hierarchy, StackMapFrame, VerificationType, compute_frames, parse, passthrough_needed, write,
};
pub use insn::{
Arith, ArrayKind, BranchCond, Cond, Const, Convert, Insn, InvokeKind, Label, Prim, SlotKind,
};
pub use model::builder::{ClassBuilder, empty_code};
pub use model::{
Attribute, BootstrapMethod, ClassFile, Code, ExceptionHandler, Field, InnerClass, LineNumber,
LocalVariable, LocalVariableType, Method, MethodParameter, Program, RecordComponent, Resource,
Version,
};

View file

@ -0,0 +1,152 @@
//! Сборка классов «с нуля»: junk-классы, рантайм-декодеры, аннотации-шум.
use crate::model::{Attribute, ClassFile, Code, Field, Method, Version};
/// Строитель класса без бинарного представления на входе.
#[derive(Debug, Clone)]
pub struct ClassBuilder {
version: Version,
access_flags: u16,
this_class: String,
super_class: Option<String>,
interfaces: Vec<String>,
fields: Vec<Field>,
methods: Vec<Method>,
attributes: Vec<Attribute>,
}
impl ClassBuilder {
/// Новый класс `public` с указанным суперклассом (`None` = `java/lang/Object`,
/// кроме самого `java/lang/Object` — у него суперкласса нет).
pub fn new(this_class: impl Into<String>, super_class: Option<&str>) -> Self {
let this_class = this_class.into();
let super_class = match super_class {
Some(name) => Some(name.to_string()),
None if this_class == "java/lang/Object" => None,
None => Some("java/lang/Object".to_string()),
};
Self {
version: Version::JAVA_21,
access_flags: 0x0021, // ACC_PUBLIC | ACC_SUPER
this_class,
super_class,
interfaces: Vec::new(),
fields: Vec::new(),
methods: Vec::new(),
attributes: Vec::new(),
}
}
/// Задать версию классового файла.
pub fn version(mut self, version: Version) -> Self {
self.version = version;
self
}
/// Задать флаги доступа (перезаписывает `ACC_PUBLIC|ACC_SUPER`).
pub fn access_flags(mut self, flags: u16) -> Self {
self.access_flags = flags;
self
}
/// Добавить интерфейс.
pub fn interface(mut self, name: impl Into<String>) -> Self {
self.interfaces.push(name.into());
self
}
/// Добавить поле.
pub fn field(mut self, name: impl Into<String>, descriptor: impl Into<String>) -> Self {
self.fields.push(Field {
access_flags: 0x0001, // ACC_PUBLIC
name: name.into(),
descriptor: descriptor.into(),
attributes: Vec::new(),
});
self
}
/// Добавить метод без тела (`abstract`/`native` должны выставить флаги сами).
pub fn method(
mut self,
access_flags: u16,
name: impl Into<String>,
descriptor: impl Into<String>,
) -> Self {
self.methods.push(Method {
access_flags,
name: name.into(),
descriptor: descriptor.into(),
code: None,
attributes: Vec::new(),
});
self
}
/// Добавить метод с телом.
pub fn method_with_code(
mut self,
access_flags: u16,
name: impl Into<String>,
descriptor: impl Into<String>,
code: Code,
) -> Self {
self.methods.push(Method {
access_flags,
name: name.into(),
descriptor: descriptor.into(),
code: Some(code),
attributes: Vec::new(),
});
self
}
/// Добавить атрибут уровня класса.
pub fn attribute(mut self, attribute: Attribute) -> Self {
self.attributes.push(attribute);
self
}
/// Собрать класс.
pub fn build(self) -> ClassFile {
ClassFile {
version: self.version,
access_flags: self.access_flags,
this_class: self.this_class,
super_class: self.super_class,
interfaces: self.interfaces,
fields: self.fields,
methods: self.methods,
attributes: self.attributes,
}
}
}
/// Пустое тело метода (`return`), для junk-методов.
pub fn empty_code(max_locals: u16) -> Code {
Code {
max_stack: 0,
max_locals,
insns: vec![crate::insn::Insn::Return(None)],
exception_table: Vec::new(),
frames: Vec::new(),
attributes: Vec::new(),
}
}
#[cfg(test)]
mod tests {
#![allow(clippy::expect_used, clippy::unwrap_used)]
use super::*;
#[test]
fn builds_class_with_method() {
let class = ClassBuilder::new("pkg/A", None)
.field("f", "I")
.method_with_code(0x0009, "m", "()V", empty_code(0))
.build();
assert_eq!(class.this_class, "pkg/A");
assert_eq!(class.fields.len(), 1);
assert!(class.method("m", "()V").unwrap().has_code());
}
}

View file

@ -0,0 +1,236 @@
//! Структура «программы» — единица работы пайплайна.
pub mod builder;
pub mod version;
pub use version::Version;
use crate::format::StackMapFrame;
use crate::insn::{Const, Insn, Label};
/// Неизвестные/специфичные атрибуты хранятся байтами — round-trip без потерь.
#[derive(Debug, Clone, PartialEq)]
pub enum Attribute {
SourceFile(String),
LineNumberTable(Vec<LineNumber>),
LocalVariableTable(Vec<LocalVariable>),
Signature(String),
Exceptions(Vec<String>),
Deprecated,
Synthetic,
InnerClasses(Vec<InnerClass>),
/// `ConstantValue` поля (`static final`): значение из пула.
ConstantValue(Const),
/// `BootstrapMethods` для `invokedynamic` (лямбды): метод + аргументы.
BootstrapMethods(Vec<BootstrapMethod>),
/// `EnclosingMethod`: класс и опциональный метод, содержащие локальный класс.
EnclosingMethod {
class: String,
method: Option<(String, String)>,
},
/// `MethodParameters`: имена формальных параметров (`None` — безымянный).
MethodParameters(Vec<MethodParameter>),
/// `Record`: компоненты record-класса.
Record(Vec<RecordComponent>),
/// `LocalVariableTypeTable`: сигнатуры generic-локальных (как LVT).
LocalVariableTypeTable(Vec<LocalVariableType>),
/// Всё остальное: имя + сырые байты `info` (осторожно с индексами пула).
Raw {
name: String,
data: Vec<u8>,
},
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct LineNumber {
/// Метка начала (байты вычисляет writer — переживает вставки пассов).
pub start: Label,
pub line: u16,
}
#[derive(Debug, Clone, PartialEq)]
pub struct LocalVariable {
/// Метки диапазона видимости (writer пересчитает в байты).
pub start: Label,
pub end: Label,
pub name: String,
pub descriptor: String,
pub index: u16,
}
#[derive(Debug, Clone, PartialEq)]
pub struct InnerClass {
pub inner: String,
pub outer: Option<String>,
pub inner_name: Option<String>,
pub access_flags: u16,
}
/// Один `bootstrap`-метод: ссылка вида `MethodHandle` + аргументы-константы.
#[derive(Debug, Clone, PartialEq)]
pub struct BootstrapMethod {
/// Номер `reference_kind` (JVMS §5.4.3.5).
pub kind: u8,
pub owner: String,
pub name: String,
pub descriptor: String,
pub itf: bool,
pub args: Vec<Const>,
}
/// Формальный параметр метода (`MethodParameters`).
#[derive(Debug, Clone, PartialEq)]
pub struct MethodParameter {
pub name: Option<String>,
pub access_flags: u16,
}
/// Компонент `Record`: поле record-класса с атрибутами.
#[derive(Debug, Clone, PartialEq)]
pub struct RecordComponent {
pub name: String,
pub descriptor: String,
pub attributes: Vec<Attribute>,
}
/// Строка `LocalVariableTypeTable` (как `LocalVariable`, но с сигнатурой).
#[derive(Debug, Clone, PartialEq)]
pub struct LocalVariableType {
/// Метки диапазона видимости (writer пересчитает в байты).
pub start: Label,
pub end: Label,
pub name: String,
pub signature: String,
pub index: u16,
}
#[derive(Debug, Clone, PartialEq)]
pub struct Field {
pub access_flags: u16,
pub name: String,
pub descriptor: String,
pub attributes: Vec<Attribute>,
}
#[derive(Debug, Clone, PartialEq)]
pub struct Method {
pub access_flags: u16,
pub name: String,
pub descriptor: String,
pub code: Option<Code>,
pub attributes: Vec<Attribute>,
}
impl Method {
/// `native`/`abstract` тела не имеют.
pub fn has_code(&self) -> bool {
self.code.is_some()
}
}
/// Тело метода.
#[derive(Debug, Clone, PartialEq)]
pub struct Code {
pub max_stack: u16,
pub max_locals: u16,
/// Маркеры меток + инструкции; ветвления указывают на [`Label`].
pub insns: Vec<Insn>,
pub exception_table: Vec<ExceptionHandler>,
pub frames: Vec<StackMapFrame>,
pub attributes: Vec<Attribute>,
}
impl Code {
/// Все метки, объявленные в теле метода.
pub fn labels(&self) -> Vec<Label> {
self.insns
.iter()
.filter_map(|i| match i {
Insn::Mark(l) => Some(*l),
_ => None,
})
.collect()
}
}
#[derive(Debug, Clone, PartialEq)]
pub struct ExceptionHandler {
/// `None` — catch-all (`finally`).
pub start: Label,
pub end: Label,
pub handler: Label,
/// Внутреннее имя класса или `None` для `any`.
pub catch_type: Option<String>,
}
/// Один `.class` в нормализованном виде.
#[derive(Debug, Clone, PartialEq)]
pub struct ClassFile {
pub version: Version,
pub access_flags: u16,
pub this_class: String,
pub super_class: Option<String>,
pub interfaces: Vec<String>,
pub fields: Vec<Field>,
pub methods: Vec<Method>,
pub attributes: Vec<Attribute>,
}
impl ClassFile {
/// Ищет метод по имени и дескриптору.
pub fn method(&self, name: &str, descriptor: &str) -> Option<&Method> {
self.methods
.iter()
.find(|m| m.name == name && m.descriptor == descriptor)
}
/// Ищет метод по имени и дескриптору (мутирующий доступ).
pub fn method_mut(&mut self, name: &str, descriptor: &str) -> Option<&mut Method> {
self.methods
.iter_mut()
.find(|m| m.name == name && m.descriptor == descriptor)
}
/// Признак `ACC_INTERFACE`.
pub fn is_interface(&self) -> bool {
self.access_flags & 0x0200 != 0
}
}
/// Ресурс внутри архива (JSON, properties, картинки, ...).
#[derive(Debug, Clone, PartialEq)]
pub struct Resource {
pub path: String,
pub data: Vec<u8>,
}
/// Вся входная программа: классы + ресурсы.
#[derive(Debug, Clone, Default, PartialEq)]
pub struct Program {
pub classes: Vec<ClassFile>,
pub resources: Vec<Resource>,
}
impl Program {
/// Класс по внутреннему имени (`pkg/Class`).
pub fn class(&self, internal_name: &str) -> Option<&ClassFile> {
self.classes.iter().find(|c| c.this_class == internal_name)
}
/// Класс по внутреннему имени (мутирующий доступ).
pub fn class_mut(&mut self, internal_name: &str) -> Option<&mut ClassFile> {
self.classes
.iter_mut()
.find(|c| c.this_class == internal_name)
}
/// Ресурс по пути внутри архива.
pub fn resource(&self, path: &str) -> Option<&Resource> {
self.resources.iter().find(|r| r.path == path)
}
/// Пустая ли программа.
pub fn is_empty(&self) -> bool {
self.classes.is_empty() && self.resources.is_empty()
}
}

View file

@ -0,0 +1,26 @@
//! Версия классового файла (major/minor).
/// Версия классового файла.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct Version {
pub minor: u16,
pub major: u16,
}
impl Version {
/// Java 8 (52).
pub const JAVA_8: Version = Version {
minor: 0,
major: 52,
};
/// Java 17 (61).
pub const JAVA_17: Version = Version {
minor: 0,
major: 61,
};
/// Java 21 (65).
pub const JAVA_21: Version = Version {
minor: 0,
major: 65,
};
}

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View 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(())
}

View 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
}

View 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}"
);
}

View 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 });
}

View file

@ -0,0 +1,20 @@
[package]
name = "noxis-core"
description = "Конфигурация, детерминированный seed, маппинги и конвейер пассов"
version.workspace = true
edition.workspace = true
rust-version.workspace = true
license.workspace = true
[dependencies]
noxis-classfile = { workspace = true }
serde = { workspace = true }
thiserror = { workspace = true }
toml = { workspace = true }
tracing = { workspace = true }
[lints]
workspace = true
[build-dependencies]
noxis-structure-check = { workspace = true }

View file

@ -0,0 +1,3 @@
fn main() {
noxis_structure_check::check();
}

View file

@ -0,0 +1,210 @@
//! Конфигурация обфускации: профили, список пассов, keep-правила.
pub mod sections;
use serde::{Deserialize, Serialize};
use self::sections::{
ConstantsConfig, DebugConfig, FlowConfig, NoiseConfig, RenameConfig, StringsConfig,
};
use crate::error::CoreError;
/// Идентификатор пасса (он же ключ в конфиге, kebab-case).
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord, Serialize, Deserialize)]
#[serde(rename_all = "kebab-case")]
pub enum PassId {
/// Переименование классов, членов, локальных переменных, пакетов.
Rename,
/// Шифрование строк + генерация рантайм-декодера.
Strings,
/// Обфускация числовых констант и настоящие opaque-предикаты.
Constants,
/// Обфускация контрольного потока.
Flow,
/// Junk-классы, junk-аннотации, ложный шум.
Noise,
/// Отладочная информация: strip / spoof.
DebugInfo,
/// Ресурсы и метаданные архива (JSON, mixins, refmap, services, AW).
Resources,
}
impl PassId {
/// Строковое имя для отчётов и логов.
pub const fn as_str(self) -> &'static str {
match self {
PassId::Rename => "rename",
PassId::Strings => "strings",
PassId::Constants => "constants",
PassId::Flow => "flow",
PassId::Noise => "noise",
PassId::DebugInfo => "debug-info",
PassId::Resources => "resources",
}
}
}
impl std::fmt::Display for PassId {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str(self.as_str())
}
}
/// Базовый набор пассов профиля.
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "kebab-case")]
pub enum Profile {
/// Только переименование + отладка. Быстро, минимальный риск сломать код.
Light,
/// Стандарт: имена, строки, константы, ресурсы, отладка, шум.
#[default]
Standard,
/// Всё включено, включая агрессивный control-flow.
Paranoid,
/// Ничего не включено явно — только `enable` из конфига.
Manual,
}
impl Profile {
/// Пассы, включённые профилем по умолчанию.
pub const fn baseline(self) -> &'static [PassId] {
match self {
Profile::Light => &[PassId::Rename, PassId::DebugInfo, PassId::Resources],
Profile::Standard => &[
PassId::Rename,
PassId::Strings,
PassId::Constants,
PassId::Noise,
PassId::DebugInfo,
PassId::Resources,
],
Profile::Paranoid => &[
PassId::Rename,
PassId::Strings,
PassId::Constants,
PassId::Flow,
PassId::Noise,
PassId::DebugInfo,
PassId::Resources,
],
Profile::Manual => &[],
}
}
}
/// Полная конфигурация (TOML).
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(default, deny_unknown_fields)]
pub struct Config {
pub profile: Profile,
/// Мастер-сид; `None` — случайный, но обязательно попадает в отчёт.
pub seed: Option<u64>,
/// Принудительно включить пассы вне профиля.
pub enable: Vec<PassId>,
/// Выключить пассы профиля.
pub disable: Vec<PassId>,
/// Keep-паттерны внутренних имён (`com/example/Main`, `*Service`).
pub keep: Vec<String>,
/// Keep-паттерны путей ресурсов, которые не переписываются.
pub keep_resources: Vec<String>,
pub rename: RenameConfig,
pub strings: StringsConfig,
pub constants: ConstantsConfig,
pub flow: FlowConfig,
pub noise: NoiseConfig,
pub debug: DebugConfig,
}
impl Default for Config {
fn default() -> Self {
Self {
profile: Profile::Standard,
seed: None,
enable: Vec::new(),
disable: Vec::new(),
keep: Vec::new(),
keep_resources: Vec::new(),
rename: RenameConfig::default(),
strings: StringsConfig::default(),
constants: ConstantsConfig::default(),
flow: FlowConfig::default(),
noise: NoiseConfig::default(),
debug: DebugConfig::default(),
}
}
}
impl Config {
/// Включён ли пасс с учётом профиля и явных override'ов.
pub fn is_enabled(&self, id: PassId) -> bool {
if self.disable.contains(&id) {
return false;
}
if self.enable.contains(&id) {
return true;
}
self.profile.baseline().contains(&id)
}
/// Загрузить конфиг из TOML.
pub fn from_toml(text: &str) -> Result<Self, CoreError> {
toml::from_str(text).map_err(|e| CoreError::Config(e.to_string()))
}
/// Нормализовать значения, которые легко задать нулем/пустым списком.
pub fn sanitize(&mut self) {
if self.rename.package_pool == 0 {
self.rename.package_pool = 1;
}
if self.strings.min_len == 0 {
self.strings.min_len = 1;
}
self.strings.schemes.retain(|s| !s.trim().is_empty());
self.flow.schemes.retain(|s| !s.trim().is_empty());
}
}
#[cfg(test)]
mod tests {
#![allow(clippy::expect_used, clippy::unwrap_used)]
use super::*;
#[test]
fn profile_gate() {
let mut cfg = Config::default();
assert!(cfg.is_enabled(PassId::Rename));
assert!(cfg.is_enabled(PassId::Strings));
assert!(!cfg.is_enabled(PassId::Flow));
cfg.profile = Profile::Paranoid;
assert!(cfg.is_enabled(PassId::Flow));
cfg.disable.push(PassId::Strings);
assert!(!cfg.is_enabled(PassId::Strings));
cfg.enable.push(PassId::Flow);
cfg.profile = Profile::Light;
assert!(cfg.is_enabled(PassId::Flow));
}
#[test]
fn parses_toml() {
let cfg = Config::from_toml(
r#"
profile = "paranoid"
seed = 123
keep = ["com/example/Main"]
[rename]
package_pool = 4
"#,
)
.unwrap();
assert_eq!(cfg.profile, Profile::Paranoid);
assert_eq!(cfg.seed, Some(123));
assert_eq!(cfg.rename.package_pool, 4);
}
#[test]
fn rejects_unknown_keys() {
assert!(Config::from_toml("totally_unknown = 1").is_err());
}
}

View file

@ -0,0 +1,185 @@
//! Секции конфигурации: стили имён и настройки отдельных пассов.
use serde::{Deserialize, Serialize};
/// Стиль генерации имён.
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "kebab-case")]
pub enum NameStyle {
/// `a`, `b`, ... `aa` — короткие латинские.
ShortLatin,
/// `I`, `l`, `O`, `0` — визуально одинаковые.
IlO0,
/// Случайные unicode-имена (суррогаты допустимы в modified UTF-8).
Unicode,
/// Смешанная латиница средней длины.
#[default]
MixedLatin,
/// Псевдосмысленные слова из другого проекта (дезориентация).
DecoyWords,
}
/// Настройки переименования.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(default, deny_unknown_fields)]
pub struct RenameConfig {
/// Переименовывать пакеты в случайные пути.
pub spread_packages: bool,
/// Размер пула случайных пакетов.
pub package_pool: usize,
/// Стиль имён классов.
pub class_style: NameStyle,
/// Стиль имён полей и методов.
pub member_style: NameStyle,
/// Стиль имён локальных переменных и параметров.
pub local_style: NameStyle,
/// Делать члены `public` (чтобы не ломать inheritance между пакетами).
pub widen_access: bool,
/// Переименовывать enum-константы.
pub rename_enum_constants: bool,
/// Переименовывать локальные переменные и параметры.
pub rename_locals: bool,
/// Переименовывать и не-private члены (агрессивно: безопасно для приложений,
/// опасно для библиотек и кода с рефлексией).
pub rename_public_members: bool,
}
impl Default for RenameConfig {
fn default() -> Self {
Self {
spread_packages: true,
package_pool: 12,
class_style: NameStyle::ShortLatin,
member_style: NameStyle::MixedLatin,
local_style: NameStyle::IlO0,
widen_access: true,
rename_enum_constants: true,
rename_locals: true,
rename_public_members: false,
}
}
}
/// Настройки шифрования строк.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(default, deny_unknown_fields)]
pub struct StringsConfig {
/// Схемы шифра, между которыми перемешиваются строки.
pub schemes: Vec<String>,
/// Строки короче этого не шифруются.
pub min_len: usize,
/// Дробить длинные строки на чанки (меньше похоже на пул).
pub split_above: usize,
/// Не шифровать строки, совпадающие с этими паттернами.
pub skip: Vec<String>,
/// Вычислять ключ из KDF(class, method, index), а не класть рядом с текстом.
pub derive_key: bool,
}
impl Default for StringsConfig {
fn default() -> Self {
Self {
schemes: vec!["xor-splitmix".to_string(), "xtea-ctr".to_string()],
min_len: 2,
split_above: 64,
skip: Vec::new(),
derive_key: true,
}
}
}
/// Настройки обфускации контрольного потока.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(default, deny_unknown_fields)]
pub struct FlowConfig {
/// Схемы, между которыми выбирается случайно по методу.
pub schemes: Vec<String>,
/// Не трогать методы больше этого числа инструкций (бюджет размера).
pub max_insns: usize,
/// Вплетать opaque-предикаты в тела блоков.
pub encrypt_block_bodies: bool,
}
impl Default for FlowConfig {
fn default() -> Self {
Self {
schemes: vec![
"state-spread".to_string(),
"computed-switch".to_string(),
"butterfly".to_string(),
"exception-goto".to_string(),
],
max_insns: 4000,
encrypt_block_bodies: false,
}
}
}
/// Настройки шифрования констант.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(default, deny_unknown_fields)]
pub struct ConstantsConfig {
/// Схемы разбиения (`xor`, `add`, `shift`), выбор по RNG на константу.
pub schemes: Vec<String>,
/// Вставлять opaque-предикаты (не больше этого на метод).
pub max_predicates: usize,
/// Не трогать константы в этих классах (префиксы внутренних имён).
pub skip: Vec<String>,
}
impl Default for ConstantsConfig {
fn default() -> Self {
Self {
schemes: vec!["xor".to_string(), "add".to_string(), "shift".to_string()],
max_predicates: 1,
skip: Vec::new(),
}
}
}
/// Настройки шума.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(default, deny_unknown_fields)]
pub struct NoiseConfig {
/// Сколько junk-классов на каждый настоящий (0 = выключено).
pub junk_ratio: usize,
/// Число junk-аннотаций на класс.
pub junk_annotations: usize,
/// Вплетать junk-классы в граф достижимости (обязательно для осмысленности).
pub weave_reachable: bool,
}
impl Default for NoiseConfig {
fn default() -> Self {
Self {
junk_ratio: 0,
junk_annotations: 0,
weave_reachable: true,
}
}
}
/// Настройки отладочной информации.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(default, deny_unknown_fields)]
pub struct DebugConfig {
/// Удалять `LineNumberTable`.
pub strip_line_numbers: bool,
/// Удалять `LocalVariableTable`.
pub strip_local_variables: bool,
/// Подменять `SourceFile` на ложное имя.
pub spoof_source_file: bool,
/// Удалять `Signature`/`InnerClasses`/`EnclosingMethod`.
pub strip_signatures: bool,
}
impl Default for DebugConfig {
fn default() -> Self {
Self {
strip_line_numbers: true,
strip_local_variables: true,
spoof_source_file: true,
strip_signatures: false,
}
}
}

View file

@ -0,0 +1,29 @@
//! Ошибки ядра пайплайна.
use crate::config::PassId;
/// Ошибка уровня ядра.
#[derive(Debug, thiserror::Error)]
pub enum CoreError {
#[error("ошибка ввода-вывода: {0}")]
Io(#[from] std::io::Error),
#[error("некорректная конфигурация: {0}")]
Config(String),
#[error("ошибка классового файла: {0}")]
Classfile(#[from] noxis_classfile::ClassfileError),
#[error("пасс `{pass}` не смог выполниться: {msg}")]
Pass { pass: PassId, msg: String },
}
impl CoreError {
/// Ошибка конкретного пасса.
pub fn pass(pass: PassId, msg: impl Into<String>) -> Self {
Self::Pass {
pass,
msg: msg.into(),
}
}
}

View file

@ -0,0 +1,25 @@
//! # noxis-core
//!
//! Ядро обфускатора: конфигурация, детерминированный seed, маппинги,
//! контекст прогона и контракт пассов ([`pipeline::Pass`]).
//!
//! Ядро ничего не знает о бинарном формате `.class` и о ZIP — только о модели
//! [`noxis_classfile::Program`]. Сборка конвейера из пассов живёт в
//! `noxis-passes`, чтобы не было цикла зависимостей.
pub mod config;
pub mod error;
pub mod mapping;
pub mod pipeline;
pub mod rng;
pub use config::sections::{
ConstantsConfig, DebugConfig, FlowConfig, NameStyle, NoiseConfig, RenameConfig, StringsConfig,
};
pub use config::{Config, PassId, Profile};
pub use error::CoreError;
pub use mapping::{Mapping, field_key, method_key};
pub use pipeline::context::Context;
pub use pipeline::report::Report;
pub use pipeline::{Pass, PassReport, Pipeline};
pub use rng::{Rng, derive_seed};

View file

@ -0,0 +1,112 @@
//! Обратная таблица маппингов: деобфускация имён и stack-trace translate.
use std::collections::BTreeMap;
use serde::Serialize;
/// Ключ метода: `owner/name+descriptor`.
pub fn method_key(owner: &str, name: &str, descriptor: &str) -> String {
format!("{owner}/{name}{descriptor}")
}
/// Ключ поля: `owner/name:descriptor`.
pub fn field_key(owner: &str, name: &str, descriptor: &str) -> String {
format!("{owner}/{name}:{descriptor}")
}
/// Таблицы «оригинал → обфусцированное».
///
/// Нужна по двум причинам: (1) перевод stack trace'ей обратно в читаемые имена
/// (аналог ZKM Stack Trace Translate), (2) аудит — что именно поменялось.
#[derive(Debug, Clone, Default, PartialEq, Serialize)]
pub struct Mapping {
/// internal name класса → новый internal name
pub classes: BTreeMap<String, String>,
/// `owner/name+desc` → новое имя метода (владелец остаётся прежним по ключу)
pub methods: BTreeMap<String, String>,
/// `owner/name:desc` → новое имя поля
pub fields: BTreeMap<String, String>,
/// имя локальной переменной/параметра в методе → новое (по номеру слота)
pub locals: BTreeMap<String, String>,
/// оригинальный путь ресурса → новый
pub resources: BTreeMap<String, String>,
}
impl Mapping {
/// Новое имя класса, если он переименован.
pub fn class(&self, from: &str) -> Option<&str> {
self.classes.get(from).map(String::as_str)
}
/// Новое имя метода.
pub fn method(&self, owner: &str, name: &str, descriptor: &str) -> Option<&str> {
self.methods
.get(&method_key(owner, name, descriptor))
.map(String::as_str)
}
/// Новое имя поля.
pub fn field(&self, owner: &str, name: &str, descriptor: &str) -> Option<&str> {
self.fields
.get(&field_key(owner, name, descriptor))
.map(String::as_str)
}
/// Число переименований всех видов.
pub fn len(&self) -> usize {
self.classes.len() + self.methods.len() + self.fields.len() + self.locals.len()
}
/// Пустая ли таблица.
pub fn is_empty(&self) -> bool {
self.len() == 0
}
/// Человекочитаемая таблица для файла маппингов и deobfuscator'а.
pub fn to_text(&self) -> String {
let mut out = String::new();
out.push_str("# classes\n");
for (from, to) in &self.classes {
out.push_str(&format!("{from} -> {to}\n"));
}
out.push_str("\n# methods\n");
for (from, to) in &self.methods {
out.push_str(&format!("{from} -> {to}\n"));
}
out.push_str("\n# fields\n");
for (from, to) in &self.fields {
out.push_str(&format!("{from} -> {to}\n"));
}
out.push_str("\n# resources\n");
for (from, to) in &self.resources {
out.push_str(&format!("{from} -> {to}\n"));
}
out
}
}
#[cfg(test)]
mod tests {
#![allow(clippy::expect_used, clippy::unwrap_used)]
use super::*;
#[test]
fn roundtrip_lookup() {
let mut m = Mapping::default();
m.classes.insert("com/example/Main".into(), "a/B".into());
m.methods
.insert(method_key("a/B", "run", "()V"), "x".into());
assert_eq!(m.class("com/example/Main"), Some("a/B"));
assert_eq!(m.method("a/B", "run", "()V"), Some("x"));
assert!(m.method("a/B", "other", "()V").is_none());
}
#[test]
fn text_report_contains_sections() {
let mut m = Mapping::default();
m.classes.insert("A".into(), "B".into());
let text = m.to_text();
assert!(text.contains("# classes"));
assert!(text.contains("A -> B"));
}
}

View file

@ -0,0 +1,94 @@
//! Контекст прогона: конфиг, мастер-сид, маппинги, заметки.
use crate::config::Config;
use crate::mapping::Mapping;
use crate::rng::{Rng, derive_seed};
/// Всё состояние, которое пассы читают и дописывают.
#[derive(Debug, Clone)]
pub struct Context {
pub config: Config,
/// Мастер-сид прогона (всегда известен и попадает в отчёт).
pub master_seed: u64,
pub mapping: Mapping,
/// Заметки пассов: почему что-то пропущено, что не удалось.
pub notes: Vec<String>,
}
impl Context {
/// Новый контекст. `seed` берётся из конфига или генерируется.
pub fn new(mut config: Config) -> Self {
config.sanitize();
let master_seed = config.seed.unwrap_or_else(fresh_seed);
Self {
config,
master_seed,
mapping: Mapping::default(),
notes: Vec::new(),
}
}
/// Детерминированный RNG для домена (`"rename"`, `"strings/0"`, ...).
///
/// Доменное разделение: разные пассы не должны вытягивать одну
/// последовательность — иначе их «случайности» статистически коррелируют.
pub fn rng(&self, domain: &str) -> Rng {
Rng::from_seed(derive_seed(self.master_seed, domain, 0))
}
/// RNG с порядковым номером внутри домена.
pub fn rng_nth(&self, domain: &str, index: u64) -> Rng {
Rng::from_seed(derive_seed(self.master_seed, domain, index))
}
/// Заметка в отчёт (видна в JSON-отчёте и в логе).
pub fn note(&mut self, msg: impl Into<String>) {
let msg = msg.into();
tracing::debug!(target: "noxis", "{msg}");
self.notes.push(msg);
}
}
/// Нестабильный сид, когда пользователь его не задал.
fn fresh_seed() -> u64 {
use std::time::{SystemTime, UNIX_EPOCH};
let nanos = SystemTime::now()
.duration_since(UNIX_EPOCH)
.map(|d| d.as_nanos() as u64)
.unwrap_or(0x5EED_C0DE);
let mut r = Rng::from_seed(nanos ^ 0xA5A5_A5A5_A5A5_A5A5);
r.next_u64()
}
#[cfg(test)]
mod tests {
#![allow(clippy::expect_used, clippy::unwrap_used)]
use super::*;
#[test]
fn rng_is_reproducible_from_config_seed() {
let cfg = Config {
seed: Some(777),
..Config::default()
};
let a = Context::new(cfg.clone());
let b = Context::new(cfg);
assert_eq!(a.master_seed, b.master_seed);
let av: Vec<u64> = {
let mut r = a.rng("rename");
(0..8).map(|_| r.next_u64()).collect()
};
let bv: Vec<u64> = {
let mut r = b.rng("rename");
(0..8).map(|_| r.next_u64()).collect()
};
assert_eq!(av, bv);
}
#[test]
fn note_is_recorded() {
let mut ctx = Context::new(Config::default());
ctx.note("тест");
assert_eq!(ctx.notes.len(), 1);
}
}

View file

@ -0,0 +1,146 @@
//! Конвейер пассов: единый порядок исполнения и отчёт.
pub mod context;
pub mod report;
use serde::Serialize;
use noxis_classfile::Program;
use self::context::Context;
use self::report::Report;
use crate::config::PassId;
use crate::error::CoreError;
/// Результат одного пасса.
#[derive(Debug, Clone, Serialize)]
pub struct PassReport {
pub pass: PassId,
/// Сколько объектов трансформировано.
pub transformed: usize,
/// Сколько объектов сознательно пропущено.
pub skipped: usize,
/// Человекочитаемые заметки пасса.
pub notes: Vec<String>,
}
impl PassReport {
/// Пустой отчёт (пасс ещё не реализован).
pub fn not_implemented(pass: PassId, todo: &'static str) -> Self {
Self {
pass,
transformed: 0,
skipped: 0,
notes: vec![format!("не реализовано: {todo}")],
}
}
}
/// Единый контракт трансформации программы.
pub trait Pass: Send + Sync {
/// Идентификатор пасса (совпадает с ключом в конфиге).
fn id(&self) -> PassId;
/// Применить пасс. Порядок пассов задаёт [`Pipeline`].
fn apply(&self, ctx: &mut Context, program: &mut Program) -> Result<PassReport, CoreError>;
}
/// Упорядоченная цепочка пассов.
///
/// Порядок — не деталь: см. `docs/architecture.md` (rename → resources →
/// strings → constants → flow → noise → debug), иначе часть техник ломает
/// предыдущие или работает по уже мёртвым именам.
#[derive(Default)]
pub struct Pipeline {
passes: Vec<Box<dyn Pass>>,
}
impl Pipeline {
/// Пустой конвейер.
pub fn new() -> Self {
Self::default()
}
/// Добавить пасс в конец.
pub fn push(mut self, pass: Box<dyn Pass>) -> Self {
self.passes.push(pass);
self
}
/// Число пассов в цепочке.
pub fn len(&self) -> usize {
self.passes.len()
}
/// Пуст ли конвейер.
pub fn is_empty(&self) -> bool {
self.passes.is_empty()
}
/// Прогнать всю программу через все пассы.
pub fn run(&self, ctx: &mut Context, program: &mut Program) -> Result<Report, CoreError> {
let mut report = Report {
seed: ctx.master_seed,
..Report::default()
};
report.bump("classes", program.classes.len());
report.bump("resources", program.resources.len());
for pass in &self.passes {
let id = pass.id();
tracing::info!(target: "noxis", pass = %id, "пасс запущен");
let result = pass.apply(ctx, program)?;
tracing::info!(
target: "noxis",
pass = %id,
transformed = result.transformed,
skipped = result.skipped,
"пасс завершён"
);
report.passes.push(result);
}
report.notes = std::mem::take(&mut ctx.notes);
Ok(report)
}
}
#[cfg(test)]
mod tests {
#![allow(clippy::expect_used, clippy::unwrap_used)]
use super::*;
struct Stub(PassId);
impl Pass for Stub {
fn id(&self) -> PassId {
self.0
}
fn apply(
&self,
_ctx: &mut Context,
_program: &mut Program,
) -> Result<PassReport, CoreError> {
Ok(PassReport {
pass: self.0,
transformed: 1,
skipped: 0,
notes: Vec::new(),
})
}
}
#[test]
fn runs_in_order_and_collects_reports() {
let pipeline = Pipeline::new()
.push(Box::new(Stub(PassId::Rename)))
.push(Box::new(Stub(PassId::Strings)));
let mut ctx = Context::new(crate::config::Config::default());
let mut program = Program::default();
let report = pipeline.run(&mut ctx, &mut program).unwrap();
assert_eq!(report.passes.len(), 2);
assert_eq!(report.passes[0].pass, PassId::Rename);
assert_eq!(report.passes[1].pass, PassId::Strings);
assert_eq!(report.counters.get("classes"), Some(&0));
}
}

View file

@ -0,0 +1,20 @@
//! Итог прогона: пассы + счётчики — сериализуется в JSON.
use serde::Serialize;
/// Итог прогона: пассы + счётчики — сериализуется в JSON.
#[derive(Debug, Clone, Default, Serialize)]
pub struct Report {
pub seed: u64,
pub passes: Vec<crate::pipeline::PassReport>,
pub notes: Vec<String>,
/// Произвольные счётчики (`classes`, `strings_encrypted`, ...).
pub counters: std::collections::BTreeMap<String, usize>,
}
impl Report {
/// Увеличить счётчик.
pub fn bump(&mut self, key: &str, by: usize) {
*self.counters.entry(key.to_string()).or_insert(0) += by;
}
}

View file

@ -0,0 +1,168 @@
//! Детерминированный PRNG и вывод производных seed'ов.
//!
//! Свой генератор, а не `rand`: обфускация должна быть **воспроизводимой между
//! версиями инструмента** — алгоритм замирает в формате, а не в изменении API
//! чужого крейта. Схема: SplitMix64 для инициализации + xoshiro256++ для генерации.
/// Генератор xoshiro256++.
#[derive(Debug, Clone)]
pub struct Rng {
s: [u64; 4],
}
const PHI: u64 = 0x9E37_79B9_7F4A_7C15;
fn splitmix64(state: &mut u64) -> u64 {
*state = state.wrapping_add(PHI);
let mut z = *state;
z = (z ^ (z >> 30)).wrapping_mul(0xBF58_476D_1CE4_E5B9);
z = (z ^ (z >> 27)).wrapping_mul(0x94D0_49BB_1331_11EB);
z ^ (z >> 31)
}
fn rotl(x: u64, k: u32) -> u64 {
x.rotate_left(k)
}
impl Rng {
/// Генератор из мастер-сиди.
pub fn from_seed(seed: u64) -> Self {
let mut sm = seed;
let s = [
splitmix64(&mut sm),
splitmix64(&mut sm),
splitmix64(&mut sm),
splitmix64(&mut sm),
];
Self { s }
}
/// Следующее 64-битное значение.
pub fn next_u64(&mut self) -> u64 {
let result = rotl(self.s[0].wrapping_add(self.s[3]), 23).wrapping_add(self.s[0]);
let t = self.s[1] << 17;
self.s[2] ^= self.s[0];
self.s[3] ^= self.s[1];
self.s[1] ^= self.s[2];
self.s[0] ^= self.s[3];
self.s[2] ^= t;
self.s[3] = rotl(self.s[3], 45);
result
}
/// Значение из `[0, bound)` без смещения (отбраковка перекоса).
pub fn below(&mut self, bound: u64) -> u64 {
assert!(bound > 0, "bound должен быть больше нуля");
// Пространство [0, 2^64) режем на куски размером bound; хвост отбрасываем.
let limit = u64::MAX - (u64::MAX % bound);
loop {
let v = self.next_u64();
if v < limit {
return v % bound;
}
}
}
/// Случайный элемент среза.
pub fn choose<'a, T>(&mut self, items: &'a [T]) -> Option<&'a T> {
if items.is_empty() {
None
} else {
Some(&items[self.below(items.len() as u64) as usize])
}
}
/// Перемешать срез на месте (Fisher–Yates).
pub fn shuffle<T>(&mut self, items: &mut [T]) {
for i in (1..items.len()).rev() {
let j = self.below((i + 1) as u64) as usize;
items.swap(i, j);
}
}
/// Случайные байты.
pub fn fill_bytes(&mut self, out: &mut [u8]) {
for chunk in out.chunks_mut(8) {
let v = self.next_u64().to_le_bytes();
let n = chunk.len();
chunk.copy_from_slice(&v[..n]);
}
}
/// Вектор случайных байтов.
pub fn bytes(&mut self, len: usize) -> Vec<u8> {
let mut v = vec![0u8; len];
self.fill_bytes(&mut v);
v
}
}
/// FNV-1a 64 — детерминированный вывод производного сида.
///
/// Разделение доменов обязательно: пассы не должны делить одну и ту же
/// последовательность (иначе переименование и шифрование «случайно»
/// коррелируют, и это видно статистикой).
pub fn derive_seed(master: u64, domain: &str, index: u64) -> u64 {
let mut hash: u64 = 0xcbf2_9ce4_8422_2325;
let mut mix = |byte: u8| {
hash ^= u64::from(byte);
hash = hash.wrapping_mul(0x0000_0100_0000_01B3);
};
for b in master.to_le_bytes() {
mix(b);
}
for b in domain.as_bytes() {
mix(*b);
}
for b in index.to_le_bytes() {
mix(b);
}
hash
}
#[cfg(test)]
mod tests {
#![allow(clippy::expect_used, clippy::unwrap_used)]
use super::*;
#[test]
fn deterministic_across_instances() {
let mut a = Rng::from_seed(42);
let mut b = Rng::from_seed(42);
let av: Vec<u64> = (0..32).map(|_| a.next_u64()).collect();
let bv: Vec<u64> = (0..32).map(|_| b.next_u64()).collect();
assert_eq!(av, bv);
}
#[test]
fn different_seeds_diverge() {
let mut a = Rng::from_seed(1);
let mut b = Rng::from_seed(2);
assert_ne!(a.next_u64(), b.next_u64());
}
#[test]
fn below_respects_bound() {
let mut r = Rng::from_seed(7);
for _ in 0..1000 {
assert!(r.below(10) < 10);
}
}
#[test]
fn derive_seed_is_domain_separated() {
assert_ne!(derive_seed(1, "rename", 0), derive_seed(1, "strings", 0));
assert_ne!(derive_seed(1, "rename", 0), derive_seed(1, "rename", 1));
assert_eq!(derive_seed(1, "rename", 3), derive_seed(1, "rename", 3));
}
#[test]
fn shuffle_is_permutation() {
let mut r = Rng::from_seed(9);
let mut v: Vec<u32> = (0..50).collect();
r.shuffle(&mut v);
let mut sorted = v.clone();
sorted.sort_unstable();
assert_eq!(sorted, (0..50).collect::<Vec<u32>>());
}
}

View file

@ -0,0 +1,18 @@
[package]
name = "noxis-jar"
description = "Чтение и запись JAR: ZIP-архив, ресурсы, срез подписей"
version.workspace = true
edition.workspace = true
rust-version.workspace = true
license.workspace = true
[dependencies]
noxis-classfile = { workspace = true }
thiserror = { workspace = true }
zip = { workspace = true }
[lints]
workspace = true
[build-dependencies]
noxis-structure-check = { workspace = true }

View file

@ -0,0 +1,3 @@
fn main() {
noxis_structure_check::check();
}

View file

@ -0,0 +1,157 @@
//! Чтение и запись ZIP-архива JAR.
//!
//! Единственная задача модуля — переместить записи из/в вектор [`Entry`],
//! сохранив порядок и не потеряв дубликатов молча. Всё, что связано с
//! содержимым (`Program`, маппинги, подпись), живёт в соседних модулях.
use std::fs::File;
use std::io::{Read, Seek, Write};
use std::path::Path;
use thiserror::Error;
use zip::CompressionMethod;
use zip::write::SimpleFileOptions;
/// Ошибки работы с архивом.
#[derive(Debug, Error)]
pub enum JarError {
#[error("io: {0}")]
Io(#[from] std::io::Error),
#[error("zip: {0}")]
Zip(#[from] zip::result::ZipError),
#[error("duplicate archive entry: {0}")]
Duplicate(String),
#[error("class file: {0}")]
Classfile(#[from] noxis_classfile::ClassfileError),
}
/// Одна запись архива: имя + байты.
///
/// Порядок в `[`Entry`]` — это порядок в архиве, он сохраняется при
/// чтении и записи. Метод сжатия не хранится: всё пишется deflate'ом,
/// что детерминированно и одинаково для одинаковых данных.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Entry {
pub name: String,
pub data: Vec<u8>,
}
impl Entry {
/// Создаёт запись по имени и байтам.
pub fn new(name: impl Into<String>, data: impl Into<Vec<u8>>) -> Self {
Self {
name: name.into(),
data: data.into(),
}
}
}
/// Читает все не-каталожные записи из `reader`, сохраняя порядок.
///
/// Повторяющееся имя — ошибка, а не тихое предупреждение: битый архив
/// с двумя `META-INF/MANIFEST.MF` собрать дороже, чем отказать.
pub fn read_entries<R: Read + Seek>(reader: R) -> Result<Vec<Entry>, JarError> {
let mut archive = zip::ZipArchive::new(reader)?;
let mut entries = Vec::with_capacity(archive.len());
for index in 0..archive.len() {
let mut file = archive.by_index(index)?;
if file.is_dir() {
continue;
}
let name = file.name().to_string();
let mut data = Vec::new();
file.read_to_end(&mut data)?;
entries.push(Entry { name, data });
}
let names: Vec<String> = entries.iter().map(|entry| entry.name.clone()).collect();
ensure_unique(&names)?;
Ok(entries)
}
/// Пишет записи в `writer` в переданном порядке.
pub fn write_entries<W: Write + Seek>(writer: W, entries: &[Entry]) -> Result<(), JarError> {
let names: Vec<String> = entries.iter().map(|entry| entry.name.clone()).collect();
ensure_unique(&names)?;
let options = SimpleFileOptions::default().compression_method(CompressionMethod::Deflated);
let mut out = zip::ZipWriter::new(writer);
for entry in entries {
out.start_file(entry.name.as_str(), options)?;
out.write_all(&entry.data)?;
}
out.finish()?;
Ok(())
}
/// Читает архив с диска.
pub fn read_file(path: &Path) -> Result<Vec<Entry>, JarError> {
read_entries(File::open(path)?)
}
/// Пишет архив на диск.
pub fn write_file(path: &Path, entries: &[Entry]) -> Result<(), JarError> {
let file = File::create(path)?;
write_entries(file, entries)
}
/// Отклоняет повторяющиеся имена записей.
fn ensure_unique(names: &[String]) -> Result<(), JarError> {
let mut seen: Vec<&str> = Vec::with_capacity(names.len());
for name in names {
if seen.contains(&name.as_str()) {
return Err(JarError::Duplicate(name.clone()));
}
seen.push(name);
}
Ok(())
}
#[cfg(test)]
mod tests {
#![allow(clippy::expect_used, clippy::unwrap_used)]
use std::io::Cursor;
use super::*;
fn entries() -> Vec<Entry> {
vec![
Entry::new("META-INF/MANIFEST.MF", b"Manifest-Version: 1.0\n".to_vec()),
Entry::new("demo/A.class", vec![0xCA, 0xFE, 0xBA, 0xBE]),
Entry::new("config/app.json", br#"{"k":1}"#.to_vec()),
]
}
#[test]
fn round_trip_preserves_order_and_bytes() {
let source = entries();
let mut buffer = Vec::new();
write_entries(Cursor::new(&mut buffer), &source).expect("write");
let restored = read_entries(Cursor::new(&buffer)).expect("read");
assert_eq!(restored, source);
}
#[test]
fn duplicate_names_are_rejected() {
let dup = vec![
Entry::new("a.txt", b"1".to_vec()),
Entry::new("a.txt", b"2".to_vec()),
];
let mut buffer = Vec::new();
let err = write_entries(Cursor::new(&mut buffer), &dup).expect_err("dup");
assert!(matches!(err, JarError::Duplicate(name) if name == "a.txt"));
}
#[test]
fn missing_file_is_an_io_error() {
let err = read_file(Path::new("/definitely/not/here.jar")).expect_err("open");
assert!(matches!(err, JarError::Io(_)));
}
}

View file

@ -0,0 +1,22 @@
//! # noxis-jar
//!
//! Чтение и запись JAR: ZIP-архив, классы, ресурсы, подписи.
//!
//! Слой разделён на три несвязанные задачи:
//!
//! * [`archive`] — чистая работа с ZIP: порядок записей, сжатие, дубликаты;
//! * [`signatures`] — срез невалидных подписей `META-INF/*.SF|RSA|DSA|EC`;
//! * [`loader`] — перевод архива в [`Program`](noxis_classfile::Program)
//! и обратно (классы через [`format`](noxis_classfile::format), остальное
//! как ресурсы).
//!
//! Обфускация ресурсов — пасс `resources` в `noxis-passes`; сюда она не
//! входит, потому что ей нужен маппинг имён, а не I/O.
pub mod archive;
pub mod loader;
pub mod signatures;
pub use archive::{Entry, JarError, read_entries, read_file, write_entries, write_file};
pub use loader::{load, save};
pub use signatures::{is_signature_entry, strip_signatures};

View file

@ -0,0 +1,100 @@
//! Перевод архива в [`Program`] и обратно.
//!
//! Правило простое: `*.class` уходит в парсер, всё остальное — в ресурсы.
//! Обратно классы сериализуются первыми, затем ресурсы: порядок внутри
//! архива на поведение не влияет, а фиксированный порядок даёт
//! воспроизводимую запись.
use noxis_classfile::Program;
use noxis_classfile::format;
use noxis_classfile::model::Resource;
use crate::archive::{Entry, JarError};
/// Разбирает записи архива: классы — через парсер, остальное — как ресурсы.
pub fn load(entries: Vec<Entry>) -> Result<Program, JarError> {
let mut program = Program::default();
for entry in entries {
if is_class_entry(&entry.name) {
program.classes.push(format::parse(&entry.data)?);
} else {
program.resources.push(Resource {
path: entry.name,
data: entry.data,
});
}
}
Ok(program)
}
/// Собирает записи архива из [`Program`]: сначала классы, потом ресурсы.
pub fn save(program: &Program) -> Result<Vec<Entry>, JarError> {
let mut entries = Vec::with_capacity(program.classes.len() + program.resources.len());
for class in &program.classes {
let name = format!("{}.class", class.this_class);
entries.push(Entry::new(name, format::write(class)?));
}
for resource in &program.resources {
entries.push(Entry::new(resource.path.clone(), resource.data.clone()));
}
Ok(entries)
}
/// Классовый файл внутри архива: путь оканчивается на `.class`.
fn is_class_entry(name: &str) -> bool {
name.len() > ".class".len() && name.ends_with(".class")
}
#[cfg(test)]
mod tests {
#![allow(clippy::expect_used, clippy::unwrap_used)]
use super::*;
#[test]
fn class_paths_are_recognised() {
assert!(is_class_entry("demo/App.class"));
assert!(is_class_entry("a/b/C.class"));
assert!(!is_class_entry(".class"));
assert!(!is_class_entry("classes.txt"));
assert!(!is_class_entry("demo/App.class.bak"));
}
#[test]
fn resources_round_trip_without_classes() {
let source = vec![
Entry::new("META-INF/MANIFEST.MF", b"Manifest-Version: 1.0\n".to_vec()),
Entry::new("config/app.json", br#"{"k":1}"#.to_vec()),
];
let program = load(source.clone()).expect("load");
assert!(program.classes.is_empty());
assert_eq!(program.resources.len(), 2);
assert_eq!(save(&program).expect("save"), source);
}
/// До P1 `format::parse` — заглушка; после P1 ожидается успешный разбор.
#[test]
fn class_entries_go_through_the_parser() {
let entries = vec![
Entry::new("demo/App.class", vec![0xCA, 0xFE]),
Entry::new("notes.txt", b"hi".to_vec()),
];
match load(entries) {
Ok(program) => {
assert_eq!(program.classes.len(), 1);
assert_eq!(program.resources.len(), 1);
}
Err(JarError::Classfile(err)) => {
assert!(!err.to_string().is_empty());
}
Err(other) => panic!("unexpected error: {other}"),
}
}
}

View file

@ -0,0 +1,103 @@
//! Срез невалидных подписей JAR.
//!
//! После перепаковки архива `META-INF/*.SF` и `*.RSA|DSA|EC` описывают
//! **старые** байты: JVM либо отказывается запускать jar, либо ругается на
//! подпись. Вырезать их обязательно — и это делается на уровне записей,
//! потому что `MANIFEST.MF` остаётся, а его секции `Name:` тоже нужно
//! выкинуть (ими занимается пасс `resources`).
use crate::archive::Entry;
/// Расширения файлов подписи в `META-INF/` (без учёта регистра).
const SIGNATURE_EXTS: [&str; 4] = [".SF", ".RSA", ".DSA", ".EC"];
/// Префикс блочных файлов подписи старого образца (`META-INF/SIG-*`).
const LEGACY_PREFIX: &str = "SIG-";
/// Путь каталога подписей.
const META_INF: &str = "META-INF/";
/// Проверяет, является ли запись архива файлом подписи.
///
/// Совпадение — только для файлов, лежащих **непосредственно** в
/// `META-INF/`: вложенный `META-INF/certs/x.SF` — обычный ресурс.
///
/// Регистр не важен (`meta-inf/x.sf` — тоже подпись), `MANIFEST.MF`
/// никогда не является подписью.
pub fn is_signature_entry(name: &str) -> bool {
let Some(relative) = name.strip_prefix(META_INF) else {
return false;
};
if relative.is_empty() || relative.contains('/') {
return false;
}
let upper = relative.to_ascii_uppercase();
if upper.starts_with(LEGACY_PREFIX) {
return true;
}
SIGNATURE_EXTS.iter().any(|ext| upper.ends_with(ext))
}
/// Удаляет подписи, возвращая оставшиеся записи и число удалённых.
///
/// Порядок остальных записей не меняется.
pub fn strip_signatures(entries: Vec<Entry>) -> (Vec<Entry>, usize) {
let before = entries.len();
let mut kept = entries;
kept.retain(|entry| !is_signature_entry(&entry.name));
let removed = before - kept.len();
(kept, removed)
}
#[cfg(test)]
mod tests {
#![allow(clippy::expect_used, clippy::unwrap_used)]
use super::*;
#[test]
fn signature_files_under_meta_inf_are_detected() {
for name in [
"META-INF/FOO.SF",
"META-INF/foo.rsa",
"META-INF/FOO.DSA",
"META-INF/FOO.EC",
"META-INF/SIG-CUSTOM",
] {
assert!(is_signature_entry(name), "{name} should be a signature");
}
}
#[test]
fn manifest_and_resources_are_kept() {
for name in [
"META-INF/MANIFEST.MF",
"META-INF/services/demo.Loader",
"META-INF/certs/root.SF",
"demo/App.class",
"NOT-META-INF/x.SF",
] {
assert!(!is_signature_entry(name), "{name} must be kept");
}
}
#[test]
fn strip_removes_only_signatures_and_counts_them() {
let entries = vec![
Entry::new("META-INF/MANIFEST.MF", b"m".to_vec()),
Entry::new("META-INF/C.SF", b"s".to_vec()),
Entry::new("demo/A.class", b"c".to_vec()),
Entry::new("META-INF/C.RSA", b"b".to_vec()),
];
let (kept, removed) = strip_signatures(entries);
assert_eq!(removed, 2);
assert_eq!(
kept.iter()
.map(|entry| entry.name.as_str())
.collect::<Vec<_>>(),
vec!["META-INF/MANIFEST.MF", "demo/A.class"]
);
}
}

View file

@ -0,0 +1,21 @@
[package]
name = "noxis-passes"
description = "Трансформации: переименование, строки, константы, CFG, шум, отладка"
version.workspace = true
edition.workspace = true
rust-version.workspace = true
license.workspace = true
[dependencies]
noxis-analysis = { workspace = true }
noxis-classfile = { workspace = true }
noxis-core = { workspace = true }
noxis-runtime = { workspace = true }
thiserror = { workspace = true }
tracing = { workspace = true }
[lints]
workspace = true
[build-dependencies]
noxis-structure-check = { workspace = true }

View file

@ -0,0 +1,3 @@
fn main() {
noxis_structure_check::check();
}

View file

@ -0,0 +1,159 @@
//! Схемы разбиения констант и opaque-предикаты.
//!
//! Целые константы вне `iconst`-диапазона заменяются тройками (`x^k`,
//! `a+b`, сдвиг); opaque-предикат — всегда истинное условие с мёртвой
//! ветвью из одного `nop` (валидно, не исполняется, ломает линейный
//! дисассемблирование).
use noxis_classfile::{Arith, BranchCond, Cond, Const, Insn, Prim};
use noxis_core::Rng;
/// Схема разбиения (имена из `ConstantsConfig::schemes`).
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(super) enum Scheme {
Xor,
Add,
Shift,
}
impl Scheme {
pub(super) fn parse(name: &str) -> Option<Self> {
match name {
"xor" => Some(Scheme::Xor),
"add" => Some(Scheme::Add),
"shift" => Some(Scheme::Shift),
_ => None,
}
}
}
/// Разбить `int`-константу на вычисляющую её тройку.
pub(super) fn encrypt_int(value: i32, scheme: Scheme, rng: &mut Rng) -> Vec<Insn> {
let (first, second) = match scheme {
Scheme::Xor => {
let mut key = rng.next_u64() as i32;
if key == 0 {
key = 0x5BD1_E995u32 as i32;
}
(value ^ key, key)
}
Scheme::Add => {
let part = rng.next_u64() as i32;
(part, value.wrapping_sub(part))
}
Scheme::Shift => (value >> 1, value),
};
match scheme {
Scheme::Xor => binary(first, second, Arith::Xor, Prim::Int),
Scheme::Add => binary(first, second, Arith::Add, Prim::Int),
Scheme::Shift => shift_form(value),
}
}
/// Разбить `long`-константу (те же три схемы на `l*`-опкодах).
pub(super) fn encrypt_long(value: i64, scheme: Scheme, rng: &mut Rng) -> Vec<Insn> {
let (first, second) = match scheme {
Scheme::Xor => {
let mut key = rng.next_u64() as i64;
if key == 0 {
key = 0x5BD1_E995_7A93_2AA9u64 as i64;
}
(value ^ key, key)
}
Scheme::Add => {
let part = rng.next_u64() as i64;
(part, value.wrapping_sub(part))
}
Scheme::Shift => (value >> 1, value),
};
match scheme {
Scheme::Xor => binary_long(first, second, Arith::Xor),
Scheme::Add => binary_long(first, second, Arith::Add),
Scheme::Shift => shift_form_long(value),
}
}
/// `(a, b, op)` как три инструкции.
fn binary(first: i32, second: i32, op: Arith, prim: Prim) -> Vec<Insn> {
vec![
Insn::Const(Const::Int(first)),
Insn::Const(Const::Int(second)),
Insn::Binary { prim, op },
]
}
fn binary_long(first: i64, second: i64, op: Arith) -> Vec<Insn> {
vec![
Insn::Const(Const::Long(first)),
Insn::Const(Const::Long(second)),
Insn::Binary {
prim: Prim::Long,
op,
},
]
}
/// `v = ((v >> 1) << 1) | (v & 1)` — работает для любых, включая отрицательные.
fn shift_form(value: i32) -> Vec<Insn> {
vec![
Insn::Const(Const::Int(value >> 1)),
Insn::Const(Const::Int(1)),
Insn::Binary {
prim: Prim::Int,
op: Arith::Shl,
},
Insn::Const(Const::Int(value & 1)),
Insn::Binary {
prim: Prim::Int,
op: Arith::Or,
},
]
}
/// То же для `long` (величина сдвига — всегда `int`!).
fn shift_form_long(value: i64) -> Vec<Insn> {
vec![
Insn::Const(Const::Long(value >> 1)),
Insn::Const(Const::Int(1)),
Insn::Binary {
prim: Prim::Long,
op: Arith::Shl,
},
Insn::Const(Const::Long(value & 1)),
Insn::Binary {
prim: Prim::Long,
op: Arith::Or,
},
]
}
/// Opaque-предикат `(C*C + C) & 1 == 0` (истина для любого `C`):
/// `[pred, ifne DEAD, goto MERGE, DEAD: nop, MERGE:]`, дальше — оригинал.
///
/// Метки выдаёт вызывающий код (свободные номера метода).
pub(super) fn opaque_true(dead: u32, merge: u32, rng: &mut Rng) -> Vec<Insn> {
let constant = rng.next_u64() as i32;
vec![
Insn::Const(Const::Int(constant)),
Insn::Dup,
Insn::Binary {
prim: Prim::Int,
op: Arith::Mul,
},
Insn::Const(Const::Int(constant)),
Insn::Binary {
prim: Prim::Int,
op: Arith::Add,
},
Insn::Const(Const::Int(1)),
Insn::Binary {
prim: Prim::Int,
op: Arith::And,
},
Insn::Branch(BranchCond::Zero(Cond::Ne), dead),
Insn::Goto(merge),
Insn::Mark(dead),
Insn::Nop,
Insn::Mark(merge),
]
}

View file

@ -0,0 +1,215 @@
//! Пасс шифрования констант: числа → арифметика, плюс opaque-предикаты.
//!
//! `float`/`double` не трогаются (битовая точность через целые — отдельно).
//! Мелочь из `iconst`-диапазона (`-1..=5`, `0L/1L`) остаётся как есть:
//! её шифрование только раздувает горячий код.
mod forms;
#[cfg(test)]
mod tests;
use noxis_analysis::ClassHierarchy;
use noxis_classfile::{Const, Insn, Program, compute_frames};
use noxis_core::{Context, CoreError, Pass, PassId, PassReport};
use forms::{Scheme, encrypt_int, encrypt_long, opaque_true};
/// Пасс шифрования констант.
#[derive(Debug, Default, Clone, Copy)]
pub struct ConstantsPass;
impl Pass for ConstantsPass {
fn id(&self) -> PassId {
PassId::Constants
}
fn apply(&self, ctx: &mut Context, program: &mut Program) -> Result<PassReport, CoreError> {
let cfg = ctx.config.constants.clone();
let mut schemes = Vec::with_capacity(cfg.schemes.len());
for name in &cfg.schemes {
schemes.push(
Scheme::parse(name)
.ok_or_else(|| pass_error(format!("неизвестная схема `{name}`")))?,
);
}
if schemes.is_empty() {
return Err(pass_error("нет схем шифрования"));
}
let mut rng = ctx.rng("constants");
let mut replaced = 0usize;
let mut predicates = 0usize;
let mut skipped = 0usize;
let mut mutated: Vec<(usize, usize)> = Vec::new();
for (ci, class) in program.classes.iter_mut().enumerate() {
if cfg
.skip
.iter()
.any(|prefix| class.this_class.starts_with(prefix))
{
skipped += 1;
continue;
}
for (mi, method) in class.methods.iter_mut().enumerate() {
let Some(code) = method.code.as_mut() else {
continue;
};
let mut fresh = Vec::with_capacity(code.insns.len());
let mut touched = false;
for insn in code.insns.drain(..) {
match insn {
Insn::Const(Const::Int(value)) if !is_tiny_int(value) => {
let scheme = schemes[rng.below(schemes.len() as u64) as usize];
fresh.extend(encrypt_int(value, scheme, &mut rng));
replaced += 1;
touched = true;
}
Insn::Const(Const::Long(value)) if value != 0 && value != 1 => {
let scheme = schemes[rng.below(schemes.len() as u64) as usize];
fresh.extend(encrypt_long(value, scheme, &mut rng));
replaced += 1;
touched = true;
}
other => fresh.push(other),
}
}
code.insns = fresh;
if touched {
code.max_stack = code.max_stack.saturating_add(2);
mutated.push((ci, mi));
}
}
}
insert_predicates(
program,
&mut mutated,
cfg.max_predicates,
&mut rng,
&mut predicates,
)?;
let hierarchy = ClassHierarchy::build(program);
for (ci, mi) in &mutated {
refresh_frames(program, *ci, *mi, &hierarchy)?;
}
Ok(report(replaced, predicates, skipped))
}
}
/// Мелочь, которую шифровать невыгодно (уже `iconst`).
fn is_tiny_int(value: i32) -> bool {
(-1..=5).contains(&value)
}
/// Opaque-предикаты в начало методов с кодом (не больше лимита на метод).
fn insert_predicates(
program: &mut Program,
mutated: &mut Vec<(usize, usize)>,
max_predicates: usize,
rng: &mut noxis_core::Rng,
predicates: &mut usize,
) -> Result<(), CoreError> {
if max_predicates == 0 {
return Ok(());
}
let classes = program.classes.len();
for ci in 0..classes {
let methods = program.classes[ci].methods.len();
for mi in 0..methods {
let empty_or_missing = program.classes[ci].methods[mi]
.code
.as_ref()
.is_none_or(|code| code.insns.is_empty());
if empty_or_missing {
continue;
}
let missing = || pass_error("метод пропал при вставке предиката");
let mut top = program.classes[ci]
.methods
.get(mi)
.and_then(|method| method.code.as_ref())
.ok_or_else(missing)?
.labels()
.into_iter()
.max()
.unwrap_or(0);
for _ in 0..max_predicates {
top += 1;
let dead = top;
top += 1;
let merge = top;
let at = program.classes[ci]
.methods
.get(mi)
.and_then(|method| method.code.as_ref())
.ok_or_else(missing)?
.insns
.iter()
.position(|insn| !matches!(insn, Insn::Mark(_)))
.unwrap_or(0);
let code = program.classes[ci]
.methods
.get_mut(mi)
.and_then(|method| method.code.as_mut())
.ok_or_else(missing)?;
let mut block = opaque_true(dead, merge, rng);
block.extend(code.insns.drain(at..));
code.insns.splice(at..at, block);
code.max_stack = code.max_stack.saturating_add(2);
*predicates += 1;
}
if !mutated.contains(&(ci, mi)) {
mutated.push((ci, mi));
}
}
}
Ok(())
}
/// Пересчитать фреймы затронутого метода.
fn refresh_frames(
program: &mut Program,
ci: usize,
mi: usize,
hierarchy: &ClassHierarchy,
) -> Result<(), CoreError> {
let (name, snapshot) = {
let class = program
.classes
.get(ci)
.ok_or_else(|| pass_error("класс пропал"))?;
(
class.this_class.clone(),
class
.methods
.get(mi)
.cloned()
.ok_or_else(|| pass_error("метод пропал"))?,
)
};
let frames =
compute_frames(&name, &snapshot, hierarchy).map_err(|e| pass_error(e.to_string()))?;
program
.classes
.get_mut(ci)
.and_then(|c| c.methods.get_mut(mi))
.and_then(|m| m.code.as_mut())
.ok_or_else(|| pass_error("метод пропал"))?
.frames = frames;
Ok(())
}
fn report(replaced: usize, predicates: usize, skipped: usize) -> PassReport {
PassReport {
pass: PassId::Constants,
transformed: replaced + predicates,
skipped,
notes: vec![format!(
"констант разбито: {replaced}, предикатов: {predicates}"
)],
}
}
fn pass_error(msg: impl Into<String>) -> CoreError {
CoreError::pass(PassId::Constants, msg)
}

View file

@ -0,0 +1,111 @@
//! Тесты пасса констант: формы вычисляют исходник, мелочь цела.
#![allow(clippy::expect_used, clippy::unwrap_used)]
use noxis_classfile::{ClassBuilder, Code, Const, Insn, Prim, Program, Version};
use noxis_core::{Config, Context, Pass};
use super::ConstantsPass;
use super::forms::{Scheme, encrypt_int, encrypt_long};
/// Формы обратимы арифметически (интерпретатор троек/пятёрок).
fn eval_int(insns: &[Insn]) -> i32 {
match insns {
[
Insn::Const(Const::Int(a)),
Insn::Const(Const::Int(b)),
Insn::Binary { op, .. },
] => {
use noxis_classfile::Arith::{Add, Xor};
match op {
Xor => a ^ b,
Add => a.wrapping_add(*b),
_ => panic!("не та тройка: {insns:?}"),
}
}
[
Insn::Const(Const::Int(a)),
Insn::Const(Const::Int(1)),
Insn::Binary { .. },
Insn::Const(Const::Int(b)),
Insn::Binary { .. },
] => (a << 1) | (b & 1),
_ => panic!("не форма: {insns:?}"),
}
}
#[test]
fn int_forms_round_trip() {
let mut rng = noxis_core::Rng::from_seed(11);
for value in [-1000, -7, -6, 6, 7, 42, 65536, i32::MAX, i32::MIN] {
for scheme in [Scheme::Xor, Scheme::Add, Scheme::Shift] {
assert_eq!(eval_int(&encrypt_int(value, scheme, &mut rng)), value);
}
}
}
#[test]
fn long_forms_keep_shape() {
let mut rng = noxis_core::Rng::from_seed(12);
for scheme in [Scheme::Xor, Scheme::Add, Scheme::Shift] {
let insns = encrypt_long(1_000_000_000_000, scheme, &mut rng);
assert!(matches!(
insns.last(),
Some(Insn::Binary {
prim: Prim::Long,
..
})
));
}
}
fn sample() -> Program {
let code = Code {
max_stack: 1,
max_locals: 0,
insns: vec![
Insn::Const(Const::Int(100)),
Insn::Const(Const::Int(3)),
Insn::Const(Const::Long(10)),
Insn::Return(None),
],
exception_table: Vec::new(),
frames: Vec::new(),
attributes: Vec::new(),
};
Program {
classes: vec![
ClassBuilder::new("demo/App", Some("java/lang/Object"))
.version(Version::JAVA_21)
.method_with_code(0x0009, "run", "()V", code)
.build(),
],
resources: Vec::new(),
}
}
/// Пасс: большие константы разбиты, мелочь и структура целы.
#[test]
fn constants_split_and_predicated() {
let mut program = sample();
let mut ctx = Context::new(Config {
seed: Some(9),
..Config::default()
});
let report = ConstantsPass
.apply(&mut ctx, &mut program)
.expect("constants");
let code = program.classes[0].methods[0].code.as_ref().expect("код");
let has = |v: i32| code.insns.contains(&Insn::Const(Const::Int(v)));
assert!(!has(100), "исходная константа уцелела");
assert!(has(3), "мелочь тронута");
assert!(
!code.insns.contains(&Insn::Const(Const::Long(10))),
"long уцелел"
);
// Opaque-предикат: мёртвая ветвь из `nop`.
assert!(code.insns.contains(&Insn::Nop), "нет предиката");
assert!(report.transformed >= 3, "мало трансформаций: {report:?}");
assert_eq!(report.skipped, 0);
}

View file

@ -0,0 +1,246 @@
//! Пасс отладочной информации: удаление и ложная подмена.
//!
//! Источник требований — `info/evo-weaknesses.md` §3: у разобранного
//! образца оригинальные `SourceFile` у 160 классов, 10 218 `LineNumberTable`
//! и имена локальных переменных. Это дешевле всего чинить и дороже всего
//! отдавать.
//!
//! Важно: удалять недостаточно. Инструменты (JADX/CFR) верят отладке сильнее,
//! чем байткоду, поэтому `spoof_source_file` подставляет **ложное** имя —
//! консистентное, но не связанное с реальным исходником.
use noxis_classfile::{Attribute, ClassFile, Code, Method, Program};
use noxis_core::{Context, CoreError, DebugConfig, Pass, PassId, PassReport};
use crate::namegen::NameGenerator;
/// Атрибут уровня класса/метода, который мы выкидываем как «сырой».
const ENCLOSING_METHOD: &str = "EnclosingMethod";
/// Атрибут уровня метода с именами параметров (`-parameters`).
const METHOD_PARAMETERS: &str = "MethodParameters";
/// Пасс отладочной информации.
#[derive(Debug, Default, Clone, Copy)]
pub struct DebugInfoPass;
impl Pass for DebugInfoPass {
fn id(&self) -> PassId {
PassId::DebugInfo
}
fn apply(&self, ctx: &mut Context, program: &mut Program) -> Result<PassReport, CoreError> {
let cfg = ctx.config.debug.clone();
let style = ctx.config.rename.class_style;
let mut source_names =
NameGenerator::new(ctx.rng("debug/source_file"), style).for_classes();
let mut stripped = 0usize;
let mut spoofed = 0usize;
for class in &mut program.classes {
spoofed += spoof_source_file(class, &mut source_names, &cfg);
stripped += strip_signatures(&mut class.attributes, &cfg);
for method in &mut class.methods {
stripped += strip_signatures(&mut method.attributes, &cfg);
stripped += strip_parameters(method, &cfg);
if let Some(code) = method.code.as_mut() {
stripped += strip_code(code, &cfg);
}
}
for field in &mut class.fields {
stripped += strip_signatures(&mut field.attributes, &cfg);
}
}
let mut notes = Vec::new();
if stripped > 0 {
notes.push(format!("удалено атрибутов: {stripped}"));
}
if spoofed > 0 {
notes.push(format!("подменено SourceFile: {spoofed}"));
}
Ok(PassReport {
pass: PassId::DebugInfo,
transformed: stripped + spoofed,
skipped: 0,
notes,
})
}
}
/// Оставляет только атрибуты, для которых `keep` вернул `true`.
fn retain(attrs: &mut Vec<Attribute>, keep: impl FnMut(&Attribute) -> bool) -> usize {
let before = attrs.len();
attrs.retain(keep);
before - attrs.len()
}
/// Тела методов: `LineNumberTable`, `LocalVariableTable`.
fn strip_code(code: &mut Code, cfg: &DebugConfig) -> usize {
retain(&mut code.attributes, |attr| match attr {
Attribute::LineNumberTable(_) => !cfg.strip_line_numbers,
Attribute::LocalVariableTable(_) | Attribute::LocalVariableTypeTable(_) => {
!cfg.strip_local_variables
}
_ => true,
})
}
/// Уровень класса/метода/поля: `Signature`, `InnerClasses`, `EnclosingMethod`.
fn strip_signatures(attrs: &mut Vec<Attribute>, cfg: &DebugConfig) -> usize {
if !cfg.strip_signatures {
return 0;
}
retain(attrs, |attr| match attr {
Attribute::Signature(_) | Attribute::InnerClasses(_) => false,
Attribute::EnclosingMethod { .. } => false,
Attribute::Raw { name, .. } => name != ENCLOSING_METHOD,
_ => true,
})
}
/// Имена параметров метода (`MethodParameters`).
fn strip_parameters(method: &mut Method, cfg: &DebugConfig) -> usize {
if !cfg.strip_local_variables {
return 0;
}
retain(&mut method.attributes, |attr| match attr {
Attribute::MethodParameters(_) => false,
Attribute::Raw { name, .. } => name != METHOD_PARAMETERS,
_ => true,
})
}
/// Подменяет `SourceFile` ложным, но правдоподобным именем.
fn spoof_source_file(class: &mut ClassFile, names: &mut NameGenerator, cfg: &DebugConfig) -> usize {
if !cfg.spoof_source_file {
return 0;
}
let mut replaced = 0;
for attr in &mut class.attributes {
if matches!(attr, Attribute::SourceFile(_)) {
*attr = Attribute::SourceFile(format!("{}.java", names.next_name()));
replaced += 1;
}
}
replaced
}
#[cfg(test)]
mod tests {
#![allow(clippy::expect_used, clippy::unwrap_used)]
use noxis_classfile::{ClassBuilder, Insn, LineNumber, LocalVariable, Resource, Version};
use noxis_core::{Config, Context, RenameConfig};
use super::*;
fn sample() -> Program {
let code = Code {
max_stack: 0,
max_locals: 1,
insns: vec![Insn::Return(None)],
exception_table: Vec::new(),
frames: Vec::new(),
attributes: vec![
Attribute::LineNumberTable(vec![LineNumber { start: 0, line: 12 }]),
Attribute::LocalVariableTable(vec![LocalVariable {
start: 0,
end: 0,
name: "this".into(),
descriptor: "Ldemo/App;".into(),
index: 0,
}]),
],
};
let mut class = ClassBuilder::new("demo/App", Some("java/lang/Object"))
.version(Version::JAVA_21)
.method_with_code(0x0001, "run", "()V", code)
.attribute(Attribute::SourceFile("RealApp.java".into()))
.attribute(Attribute::Signature("Ljava/lang/Object;".into()))
.attribute(Attribute::InnerClasses(Vec::new()))
.build();
class.methods[0].attributes.push(Attribute::Raw {
name: METHOD_PARAMETERS.into(),
data: vec![],
});
Program {
classes: vec![class],
resources: vec![Resource {
path: "app.json".into(),
data: br#"{}"#.to_vec(),
}],
}
}
fn run(program: &mut Program, debug: DebugConfig) -> PassReport {
let config = Config {
seed: Some(7),
debug,
rename: RenameConfig {
class_style: noxis_core::NameStyle::ShortLatin,
..RenameConfig::default()
},
..Config::default()
};
let mut ctx = Context::new(config);
DebugInfoPass.apply(&mut ctx, program).expect("debug pass")
}
#[test]
fn default_config_strips_and_spoofs() {
let mut program = sample();
let report = run(&mut program, DebugConfig::default());
let class = &program.classes[0];
let code = class.methods[0].code.as_ref().expect("code");
assert!(code.attributes.is_empty(), "атрибуты кода должны уйти");
assert!(
class.methods[0].attributes.is_empty(),
"MethodParameters уходит"
);
match class.attributes[0] {
Attribute::SourceFile(ref name) => assert_ne!(name, "RealApp.java"),
_ => panic!("SourceFile должен быть первым"),
}
assert_eq!(class.attributes.len(), 3, "Signature/InnerClasses остаются");
assert!(report.transformed > 0);
}
#[test]
fn signatures_and_parameters_survive_when_disabled() {
let debug = DebugConfig {
strip_line_numbers: false,
strip_local_variables: false,
spoof_source_file: false,
strip_signatures: false,
};
let mut program = sample();
run(&mut program, debug);
let class = &program.classes[0];
assert_eq!(class.methods[0].code.as_ref().unwrap().attributes.len(), 2);
assert_eq!(class.methods[0].attributes.len(), 1);
assert_eq!(class.attributes.len(), 3);
assert_eq!(
class.attributes[0],
Attribute::SourceFile("RealApp.java".into())
);
}
#[test]
fn same_seed_gives_the_same_spoofed_name() {
let mut first = sample();
let mut second = sample();
run(&mut first, DebugConfig::default());
run(&mut second, DebugConfig::default());
assert_eq!(first.classes[0].attributes, second.classes[0].attributes);
}
}

View file

@ -0,0 +1,95 @@
//! # noxis-passes
//!
//! Трансформации байткода. Сборка конвейера из пассов (`standard_pipeline`)
//! живёт здесь же: ядро (`noxis-core`) знает только контракт [`Pass`].
pub mod constants;
pub mod debug;
pub mod namegen;
pub mod noise;
pub mod rename;
pub mod resources;
pub mod strings;
use noxis_core::Pass;
pub use constants::ConstantsPass;
pub use debug::DebugInfoPass;
pub use namegen::NameGenerator;
pub use noise::NoisePass;
pub use rename::RenamePass;
pub use resources::ResourcesPass;
pub use strings::StringsPass;
/// Стандартный конвейер в порядке [`docs/architecture.md`](../../docs/architecture.md):
/// `rename → resources → strings → constants → flow → noise → debug`.
///
/// Нереализованные пассы просто не добавлены — порядок уже соблюдён,
/// поэтому каждый следующий пасс вставляется на своё место, а не
/// дописывается в конец.
pub fn standard_pipeline() -> noxis_core::Pipeline {
noxis_core::Pipeline::new()
.push(Box::new(RenamePass) as Box<dyn Pass>)
.push(Box::new(ResourcesPass) as Box<dyn Pass>)
.push(Box::new(StringsPass) as Box<dyn Pass>)
.push(Box::new(ConstantsPass) as Box<dyn Pass>)
.push(Box::new(NoisePass) as Box<dyn Pass>)
.push(Box::new(DebugInfoPass) as Box<dyn Pass>)
}
#[cfg(test)]
mod tests {
#![allow(clippy::expect_used, clippy::unwrap_used)]
use noxis_classfile::{ClassBuilder, Code, Insn, Program, Version};
use noxis_core::{Config, Context, PassId};
use super::*;
fn sample() -> Program {
let code = Code {
max_stack: 0,
max_locals: 1,
insns: vec![Insn::Return(None)],
exception_table: Vec::new(),
frames: Vec::new(),
attributes: Vec::new(),
};
Program {
classes: vec![
ClassBuilder::new("demo/App", Some("java/lang/Object"))
.version(Version::JAVA_21)
.method_with_code(0x0001, "run", "()V", code)
.build(),
],
resources: Vec::new(),
}
}
#[test]
fn standard_pipeline_runs_end_to_end_in_order() {
let mut ctx = Context::new(Config {
seed: Some(1),
..Config::default()
});
let mut program = sample();
let report = standard_pipeline()
.run(&mut ctx, &mut program)
.expect("pipeline");
let order: Vec<_> = report.passes.iter().map(|p| p.pass).collect();
assert_eq!(
order,
vec![
PassId::Rename,
PassId::Resources,
PassId::Strings,
PassId::Constants,
PassId::Noise,
PassId::DebugInfo
]
);
assert_eq!(report.counters.get("classes"), Some(&1));
}
}

View file

@ -0,0 +1,119 @@
//! Алфавиты, ключевые слова и запрещённые имена.
//!
//! Списки вынесены отдельно: к ним обращаются и генератор, и пасс
//! переименования (резервирование keep-имён).
/// Жёсткие ключевые слова Java: имя класса/метода не может им совпадать.
pub(super) const KEYWORDS: &[&str] = &[
"abstract",
"assert",
"boolean",
"break",
"byte",
"case",
"catch",
"char",
"class",
"const",
"continue",
"default",
"do",
"double",
"else",
"enum",
"extends",
"final",
"finally",
"float",
"for",
"goto",
"if",
"implements",
"import",
"instanceof",
"int",
"interface",
"long",
"native",
"new",
"package",
"private",
"protected",
"public",
"return",
"short",
"static",
"strictfp",
"super",
"switch",
"synchronized",
"this",
"throw",
"throws",
"transient",
"try",
"void",
"volatile",
"while",
"true",
"false",
"null",
"_",
"var",
"record",
"sealed",
"permits",
"yield",
];
/// Имена, которые не стоит затирать: мешают читать/логировать и ловятся
/// инструментами до любой деобфускации.
pub(super) const AVOID_CLASS: &[&str] = &[
"Object",
"String",
"Class",
"Record",
"Enum",
"Cloneable",
"Iterable",
"Comparable",
"Thread",
"Runnable",
"Override",
"Deprecated",
"SuppressWarnings",
];
pub(super) const AVOID_MEMBER: &[&str] = &[
"toString",
"equals",
"hashCode",
"clone",
"wait",
"notify",
"notifyAll",
"finalize",
"main",
"run",
"call",
"apply",
];
pub(super) const LATIN_LOWER: &[u8] = b"abcdefghijklmnopqrstuvwxyz";
pub(super) const LATIN_UPPER: &[u8] = b"ABCDEFGHIJKLMNOPQRSTUVWXYZ";
/// Визуально неразличимые символы — ядро стиля `IlO0`.
pub(super) const CONFUSABLE: &[u8] = b"IlO01";
/// Unicode-буквы, валидные как Java-идентификаторы (Cyrillic/Greek/CJK).
pub(super) const UNICODE_LETTERS: &[char] = &[
'а', 'б', 'в', 'г', 'д', 'е', 'ж', 'з', 'и', 'к', 'л', 'м', 'н', 'п', 'р', 'с', 'т', 'у', 'ф',
'х', 'ц', 'ч', 'ш', 'щ', 'э', 'ю', 'я', 'α', 'β', 'γ', 'δ', 'ε', 'ζ', 'η', 'θ', 'λ', 'μ', 'π',
'ρ', 'σ', 'τ', 'φ', 'ω', '日', '本', '語', '中', '文', '国',
];
pub(super) const DECOY_LEFT: &[&str] = &[
"broken", "silent", "quiet", "amber", "cobalt", "frozen", "hollow", "vivid", "mellow",
"scarlet",
];
pub(super) const DECOY_RIGHT: &[&str] = &[
"mirror", "lantern", "harbor", "ember", "shadow", "quarry", "meadow", "cipher", "anchor",
"ripple",
];

View file

@ -0,0 +1,215 @@
//! Генерация имён: стили, анти-ключевые слова, уникальность в scope.
pub mod alphabet;
use std::collections::HashSet;
use noxis_core::{NameStyle, Rng};
use self::alphabet::{
AVOID_CLASS, AVOID_MEMBER, CONFUSABLE, DECOY_LEFT, DECOY_RIGHT, KEYWORDS, LATIN_LOWER,
LATIN_UPPER, UNICODE_LETTERS,
};
/// Генератор имён с гарантированной уникальностью в пределах одного scope.
pub struct NameGenerator {
rng: Rng,
style: NameStyle,
used: HashSet<String>,
/// Счётчик-страховка, если случайные варианты исчерпаны.
fallback: u64,
avoid_class: bool,
}
impl NameGenerator {
/// Генератор для указанного стиля.
pub fn new(rng: Rng, style: NameStyle) -> Self {
Self {
rng,
style,
used: HashSet::new(),
fallback: 0,
avoid_class: false,
}
}
/// Режим «имена классов»: дополнительно избегаем имён из JDK/аннотаций.
pub fn for_classes(mut self) -> Self {
self.avoid_class = true;
self
}
/// Занять конкретное имя (защита от коллизий с keep-именами).
pub fn reserve(&mut self, name: impl Into<String>) {
self.used.insert(name.into());
}
/// Следующее свободное имя.
pub fn next_name(&mut self) -> String {
for _ in 0..64 {
let candidate = self.candidate();
if self.acceptable(&candidate) {
self.used.insert(candidate.clone());
return candidate;
}
}
// Страховка: детерминированный счётчик, гарантированно уникален.
loop {
self.fallback += 1;
let candidate = format!("{}{}", self.style_prefix(), self.fallback);
if self.acceptable(&candidate) {
self.used.insert(candidate.clone());
return candidate;
}
}
}
fn style_prefix(&self) -> &'static str {
match self.style {
NameStyle::ShortLatin | NameStyle::IlO0 | NameStyle::MixedLatin => "z",
NameStyle::Unicode => "\u{043e}",
NameStyle::DecoyWords => "tmp",
}
}
fn candidate(&mut self) -> String {
match self.style {
NameStyle::ShortLatin => {
let len = 1 + self.rng.below(3) as usize;
random_from(LATIN_LOWER, len, &mut self.rng)
}
NameStyle::IlO0 => {
let len = 3 + self.rng.below(5) as usize;
random_from(CONFUSABLE, len, &mut self.rng)
}
NameStyle::MixedLatin => {
let len = 6 + self.rng.below(7) as usize;
let mut out = String::with_capacity(len);
for i in 0..len {
let alpha = if self.rng.below(2) == 0 {
LATIN_LOWER
} else {
LATIN_UPPER
};
let idx = self.rng.below(alpha.len() as u64) as usize;
let ch = alpha[idx] as char;
// Первый символ — только буква (не цифра).
if i == 0 && !ch.is_ascii_alphabetic() {
out.push('a');
} else {
out.push(ch);
}
}
out
}
NameStyle::Unicode => {
let len = 4 + self.rng.below(6) as usize;
(0..len)
.map(|_| {
let idx = self.rng.below(UNICODE_LETTERS.len() as u64) as usize;
UNICODE_LETTERS[idx]
})
.collect()
}
NameStyle::DecoyWords => {
let l = DECOY_LEFT[self.rng.below(DECOY_LEFT.len() as u64) as usize];
let r = DECOY_RIGHT[self.rng.below(DECOY_RIGHT.len() as u64) as usize];
let sep = if self.rng.below(2) == 0 { "_" } else { "" };
let mut name = format!("{l}{sep}{r}");
if self.rng.below(2) == 0 {
name = capitalize(&name);
}
name
}
}
}
fn acceptable(&self, name: &str) -> bool {
if name.is_empty() || self.used.contains(name) {
return false;
}
if KEYWORDS.contains(&name) {
return false;
}
if self.avoid_class && AVOID_CLASS.contains(&name) {
return false;
}
if !self.avoid_class && AVOID_MEMBER.contains(&name) {
return false;
}
let Some(first) = name.chars().next() else {
return false;
};
if !first.is_alphabetic() && first != '_' && first != '$' {
return false;
}
name.chars()
.all(|c| c.is_alphanumeric() || c == '_' || c == '$')
}
}
fn random_from(alphabet: &[u8], len: usize, rng: &mut Rng) -> String {
(0..len)
.map(|_| {
let idx = rng.below(alphabet.len() as u64) as usize;
alphabet[idx] as char
})
.collect()
}
fn capitalize(s: &str) -> String {
let mut chars = s.chars();
match chars.next() {
Some(c) => c.to_uppercase().collect::<String>() + chars.as_str(),
None => String::new(),
}
}
#[cfg(test)]
mod tests {
#![allow(clippy::expect_used, clippy::unwrap_used)]
use super::alphabet::{AVOID_MEMBER, KEYWORDS};
use super::*;
#[test]
fn names_are_unique_and_valid() {
let mut names = NameGenerator::new(Rng::from_seed(1), NameStyle::ShortLatin);
let mut seen = HashSet::new();
for _ in 0..500 {
let name = names.next_name();
assert!(seen.insert(name.clone()), "повтор имени {name}");
assert!(!KEYWORDS.contains(&name.as_str()));
assert!(name.starts_with(|c: char| c.is_ascii_alphabetic()));
}
}
#[test]
fn avoids_reserved_member_names() {
let mut names = NameGenerator::new(Rng::from_seed(2), NameStyle::DecoyWords);
for _ in 0..500 {
let name = names.next_name();
assert!(!AVOID_MEMBER.contains(&name.as_str()));
}
}
#[test]
fn unicode_style_produces_identifier_valid_names() {
let mut names = NameGenerator::new(Rng::from_seed(3), NameStyle::Unicode);
for _ in 0..100 {
let name = names.next_name();
assert!(
name.chars()
.all(|c| c.is_alphanumeric() || c == '_' || c == '$')
);
}
}
#[test]
fn reserved_names_never_reissued() {
let mut names = NameGenerator::new(Rng::from_seed(4), NameStyle::ShortLatin);
names.reserve("abc");
for _ in 0..300 {
assert_ne!(names.next_name(), "abc");
}
}
}

View file

@ -0,0 +1,189 @@
//! Пасс шума: junk-классы и вплетание их в граф достижимости.
//!
//! Мёртвый код выкидывается shrink-анализом, поэтому каждый junk-класс
//! получает ребро из живого метода (`invokestatic shadow/decoyN.run()V`).
//! Junk-аннотации не генерируются: сырые атрибуты не несут свежие индексы пула.
mod weave;
use noxis_classfile::{ClassBuilder, Program, Version, empty_code};
use noxis_core::{Context, CoreError, Pass, PassId, PassReport};
use weave::weave_first_reachable;
/// Префикс детерминированных имён junk-классов.
const JUNK_PREFIX: &str = "shadow/decoy";
/// Имя и дескриптор точки входа junk-класса.
const JUNK_ENTRY: (&str, &str) = ("run", "()V");
/// Флаги `public static` для junk-метода.
const ACC_PUBLIC_STATIC: u16 = 0x0009;
/// Пасс шума.
#[derive(Debug, Default, Clone, Copy)]
pub struct NoisePass;
impl Pass for NoisePass {
fn id(&self) -> PassId {
PassId::Noise
}
fn apply(&self, ctx: &mut Context, program: &mut Program) -> Result<PassReport, CoreError> {
let ratio = ctx.config.noise.junk_ratio;
let annotations = ctx.config.noise.junk_annotations;
let weave = ctx.config.noise.weave_reachable;
if ratio == 0 && annotations == 0 {
return Ok(PassReport {
pass: PassId::Noise,
transformed: 0,
skipped: 0,
notes: vec!["шум выключен: junk_ratio=0, junk_annotations=0".to_string()],
});
}
let version = program
.classes
.first()
.map_or(Version::JAVA_21, |c| c.version);
let real_classes = program.classes.len();
let count = ratio.saturating_mul(real_classes);
let mut junk_names = Vec::with_capacity(count);
let mut suffix = 0usize;
for _ in 0..count {
let name = loop {
let candidate = format!("{JUNK_PREFIX}{suffix}");
suffix = suffix.saturating_add(1);
if program.class(&candidate).is_none() {
break candidate;
}
};
let junk = ClassBuilder::new(name.clone(), None)
.version(version)
.method_with_code(ACC_PUBLIC_STATIC, JUNK_ENTRY.0, JUNK_ENTRY.1, empty_code(0))
.build();
program.classes.push(junk);
junk_names.push(name);
}
let added = junk_names.len();
let mut woven = 0usize;
if weave && added > 0 {
woven = weave_first_reachable(program, real_classes, &junk_names)?;
}
let mut skipped = 0usize;
let mut notes = Vec::new();
if added > 0 {
notes.push(format!("добавлено junk-классов: {added}"));
}
if woven > 0 {
notes.push(format!("вплетено вызовов: {woven}"));
}
if added == 0 && woven == 0 {
notes.push("нечего шуметь: нет классов или методов с кодом".to_string());
}
if annotations > 0 {
skipped = annotations.saturating_mul(program.classes.len());
notes.push(format!(
"junk_annotations пропущен: сырые атрибуты не могут нести свежие индексы пула (пропущено: {skipped})"
));
}
for note in &notes {
ctx.note(note.clone());
}
Ok(PassReport {
pass: PassId::Noise,
transformed: added + woven,
skipped,
notes,
})
}
}
#[cfg(test)]
mod tests {
#![allow(clippy::expect_used, clippy::unwrap_used)]
use noxis_classfile::{Insn, InvokeKind, Resource};
use noxis_core::{Config, NoiseConfig};
use super::*;
fn sample() -> Program {
Program {
classes: vec![
ClassBuilder::new("demo/App", Some("java/lang/Object"))
.version(Version::JAVA_21)
.method_with_code(ACC_PUBLIC_STATIC, "run", "()V", empty_code(0))
.build(),
],
resources: vec![Resource {
path: "app.json".into(),
data: br#"{}"#.to_vec(),
}],
}
}
fn run(program: &mut Program, noise: NoiseConfig) -> PassReport {
let config = Config {
seed: Some(42),
noise,
..Config::default()
};
let mut ctx = Context::new(config);
NoisePass.apply(&mut ctx, program).expect("noise pass")
}
#[test]
fn adds_junk_and_weaves_call() {
let mut program = sample();
let noise = NoiseConfig {
junk_ratio: 2,
junk_annotations: 0,
weave_reachable: true,
};
let report = run(&mut program, noise);
assert_eq!(program.classes.len(), 3);
assert_eq!(program.classes[1].this_class, "shadow/decoy0");
assert_eq!(program.classes[2].this_class, "shadow/decoy1");
let insns = &program.classes[0].methods[0].code.as_ref().unwrap().insns;
assert_eq!(insns.len(), 2);
match &insns[0] {
Insn::Invoke {
kind,
owner,
name,
descriptor,
itf,
} => {
assert_eq!(*kind, InvokeKind::Static);
assert_eq!(owner, "shadow/decoy0");
assert_eq!(name, "run");
assert_eq!(descriptor, "()V");
assert!(!itf);
}
other => panic!("ожидался Invoke, получено: {other:?}"),
}
assert!(matches!(insns[1], Insn::Return(None)));
assert_eq!(report.transformed, 3);
assert_eq!(report.skipped, 0);
}
#[test]
fn disabled_ratio_changes_nothing() {
let mut program = sample();
let before = program.clone();
let report = run(&mut program, NoiseConfig::default());
assert_eq!(program, before);
assert_eq!(report.transformed, 0);
assert_eq!(report.skipped, 0);
}
}

View file

@ -0,0 +1,80 @@
//! Вплетание junk-классов в граф достижимости живых методов.
use noxis_analysis::ClassHierarchy;
use noxis_classfile::{Insn, InvokeKind, Program, compute_frames};
use noxis_core::{CoreError, PassId};
use super::JUNK_ENTRY;
/// Вплетает вызов первого junk-класса в первый метод с кодом.
///
/// Вызов встаёт перед терминалом (`Return`/`AReturn`/`AThrow`), чтобы не
/// породить недостижимый хвост без фреймов; фреймы пересчитываются.
pub(super) fn weave_first_reachable(
program: &mut Program,
real_classes: usize,
junk_names: &[String],
) -> Result<usize, CoreError> {
let missing = || CoreError::pass(PassId::Noise, "цель вплетания пропала");
let Some(owner) = junk_names.first() else {
return Err(missing());
};
let target = program.classes[..real_classes.min(program.classes.len())]
.iter()
.enumerate()
.find_map(|(ci, c)| {
c.methods
.iter()
.position(|m| m.code.is_some())
.map(|mi| (ci, mi))
});
let Some((ci, mi)) = target else { return Ok(0) };
let class_name = program
.classes
.get(ci)
.ok_or_else(missing)?
.this_class
.clone();
let call = Insn::Invoke {
kind: InvokeKind::Static,
owner: owner.clone(),
name: JUNK_ENTRY.0.to_string(),
descriptor: JUNK_ENTRY.1.to_string(),
itf: false,
};
{
let code = program
.classes
.get_mut(ci)
.and_then(|c| c.methods.get_mut(mi))
.and_then(|m| m.code.as_mut())
.ok_or_else(missing)?;
if matches!(
code.insns.last(),
Some(Insn::Return(_) | Insn::AReturn | Insn::AThrow)
) {
code.insns.insert(code.insns.len() - 1, call);
} else {
code.insns.push(call);
}
}
let snapshot = program
.classes
.get(ci)
.and_then(|c| c.methods.get(mi))
.cloned()
.ok_or_else(missing)?;
let hierarchy = ClassHierarchy::build(program);
let frames = compute_frames(&class_name, &snapshot, &hierarchy)
.map_err(|e| CoreError::pass(PassId::Noise, e.to_string()))?;
program
.classes
.get_mut(ci)
.and_then(|c| c.methods.get_mut(mi))
.and_then(|m| m.code.as_mut())
.ok_or_else(missing)?
.frames = frames;
Ok(1)
}

View file

@ -0,0 +1,237 @@
//! Назначение новых имён: карты, семейства виртуальных методов,
//! пул пакетов и финальное ширинирование доступа.
use std::collections::{BTreeMap, BTreeSet};
use noxis_analysis::{Analysis, ClassHierarchy};
use noxis_classfile::Program;
use noxis_core::{Config, Context, NameStyle, Rng};
use super::{ACC_PRIVATE, ACC_PROTECTED, ACC_PUBLIC, ACC_STATIC};
use crate::namegen::NameGenerator;
/// `(owner, name, descriptor)` — как в JVM-ссылке.
pub(super) type MemberKey = (String, String, String);
/// Карты переименований одного прогона: `declared` — где объявлено,
/// `alias` — наследники, которые наследуют это имя без своего объявления.
#[derive(Debug, Default)]
pub(super) struct RenameMaps {
pub(super) classes: BTreeMap<String, String>,
pub(super) methods: BTreeMap<MemberKey, String>,
pub(super) methods_alias: BTreeMap<MemberKey, String>,
pub(super) fields: BTreeMap<MemberKey, String>,
pub(super) fields_alias: BTreeMap<MemberKey, String>,
}
impl RenameMaps {
pub(super) fn method(&self, owner: &str, name: &str, desc: &str) -> Option<String> {
let key = (owner.to_string(), name.to_string(), desc.to_string());
self.methods
.get(&key)
.or_else(|| self.methods_alias.get(&key))
.cloned()
}
pub(super) fn field(&self, owner: &str, name: &str, desc: &str) -> Option<String> {
let key = (owner.to_string(), name.to_string(), desc.to_string());
self.fields
.get(&key)
.or_else(|| self.fields_alias.get(&key))
.cloned()
}
/// Псевдоними: наследник вызывает член, объявленный в предке.
pub(super) fn expand(&mut self, hierarchy: &ClassHierarchy, owners: &[String]) {
let methods: Vec<(MemberKey, String)> = self
.methods
.iter()
.map(|(k, v)| (k.clone(), v.clone()))
.collect();
for ((owner, name, desc), new_name) in &methods {
for child in owners {
if child == owner || !hierarchy.is_subclass_of(child, owner) {
continue;
}
let key = (child.clone(), name.clone(), desc.clone());
if self.methods.contains_key(&key) {
continue;
}
self.methods_alias
.entry(key)
.or_insert_with(|| new_name.clone());
}
}
let fields: Vec<(MemberKey, String)> = self
.fields
.iter()
.map(|(k, v)| (k.clone(), v.clone()))
.collect();
for ((owner, name, desc), new_name) in &fields {
for child in owners {
if child == owner || !hierarchy.is_subclass_of(child, owner) {
continue;
}
let key = (child.clone(), name.clone(), desc.clone());
if self.fields.contains_key(&key) {
continue;
}
self.fields_alias
.entry(key)
.or_insert_with(|| new_name.clone());
}
}
}
}
/// Переименование виртуальных методов «семейством»: одно имя на всю иерархию,
/// включая мостовые методы с ковариантным возвратом.
pub(super) fn rename_virtual_families(
program: &Program,
analysis: &Analysis,
maps: &mut RenameMaps,
cfg: &Config,
ctx: &mut Context,
) -> usize {
// Семейство = (имя, параметры): возврат игнорируем — так захватываем
// ковариантные возвраты и мостовые методы (`ACC_BRIDGE`) разом.
let mut families: BTreeMap<(String, String), Vec<MemberKey>> = BTreeMap::new();
let mut locked_families: BTreeSet<(String, String)> = BTreeSet::new();
for class in &program.classes {
let locked_class = analysis.is_locked(&class.this_class);
for method in &class.methods {
if method.name == "<init>" || method.name == "<clinit>" {
continue;
}
if method.access_flags & (ACC_STATIC | ACC_PRIVATE) != 0 {
continue;
}
let Some(params) = params_of(&method.descriptor) else {
continue;
};
let family = (method.name.clone(), params);
if locked_class || analysis.keep.keeps_member(&method.name) {
locked_families.insert(family.clone());
}
families.entry(family).or_default().push((
class.this_class.clone(),
method.name.clone(),
method.descriptor.clone(),
));
}
}
let mut renamed = 0usize;
let mut family_index = 0u64;
for (family_key, members) in families {
if locked_families.contains(&family_key) {
continue;
}
let mut names = NameGenerator::new(
ctx.rng_nth("rename/virtual-families", family_index),
cfg.rename.member_style,
);
family_index += 1;
for (_, name, _) in &members {
names.reserve(name.clone());
}
let new_name = names.next_name();
for key in members {
if maps.methods.contains_key(&key) {
continue;
}
maps.methods.insert(key, new_name.clone());
renamed += 1;
}
}
renamed
}
/// `(<params>)<ret>` → `(<params>)`, либо `None`, если дескриптор кривой.
fn params_of(descriptor: &str) -> Option<String> {
let close = descriptor.find(')')?;
if !descriptor.starts_with('(') {
return None;
}
Some(descriptor[..=close].to_string())
}
pub(super) fn parent_package(internal: &str) -> String {
match internal.rfind('/') {
Some(i) => internal[..i].to_string(),
None => String::new(),
}
}
pub(super) fn simple_name(internal: &str) -> String {
match internal.rfind('/') {
Some(i) => internal[i + 1..].to_string(),
None => internal.to_string(),
}
}
/// Пул случайных пакетов (2–3 сегмента); при выключенном разбросе — один пустой.
pub(super) fn generate_packages(cfg: &Config, rng: &mut Rng) -> Vec<String> {
if !cfg.rename.spread_packages {
return vec![String::new()];
}
let style = match cfg.rename.class_style {
NameStyle::Unicode => NameStyle::Unicode,
NameStyle::IlO0 => NameStyle::IlO0,
_ => NameStyle::MixedLatin,
};
let mut names = NameGenerator::new(rng.clone(), style).for_classes();
let pool = cfg.rename.package_pool.max(1);
(0..pool)
.map(|_| {
let segments = 2 + rng.below(2) as usize;
let parts: Vec<String> = (0..segments).map(|_| names.next_name()).collect();
parts.join("/")
})
.collect()
}
/// Переписать дескриптор/сигнатуру: `Lpkg/Class;` → новое имя класса.
pub(super) fn rewrite_descriptor(desc: &str, classes: &BTreeMap<String, String>) -> String {
if !desc.contains('L') {
return desc.to_string();
}
let mut out = String::with_capacity(desc.len());
let mut chars = desc.chars().peekable();
while let Some(c) = chars.next() {
if c != 'L' {
out.push(c);
continue;
}
let mut name = String::new();
for c in chars.by_ref() {
if c == ';' {
break;
}
name.push(c);
}
out.push('L');
out.push_str(classes.get(&name).map_or(name.as_str(), String::as_str));
out.push(';');
}
out
}
/// Расширить доступ до `public`, чтобы переезд в другой пакет не ломал вызовы.
pub(super) fn widen_all(program: &mut Program) {
for class in program.classes.iter_mut() {
class.access_flags = widen(class.access_flags);
for field in class.fields.iter_mut() {
field.access_flags = widen(field.access_flags);
}
for method in class.methods.iter_mut() {
method.access_flags = widen(method.access_flags);
}
}
}
fn widen(flags: u16) -> u16 {
(flags & !(ACC_PRIVATE | ACC_PROTECTED)) | ACC_PUBLIC
}

View file

@ -0,0 +1,232 @@
//! Пасс переименования: классы, поля, методы, локальные переменные, дескрипторы.
//!
//! Это самая дешёвая и самая отдающая техника (см. `info/evo-weaknesses.md`:
//! у EvoVis члены не переименовывались вообще, и класс читался по `javap -p`).
//!
//! Тонкости, которые здесь учитываются:
//! - вызов `Sub.staticMethod()` может указывать владельцем подкласс — нужны
//! псевдонимы (`alias`) для всей иерархии;
//! - мостовые методы (`ACC_BRIDGE`) и ковариантные возвраты живут в одном
//! «семействе» метода — переименовывать их нужно разом;
//! - `serialVersionUID`, JNI, рефлексия, сериализация — не трогаем.
/// Карты и стратегия назначения имён.
mod assign;
/// Переписывание тел методов и атрибутов.
mod rewrite;
/// Интеграционные тесты пасса.
#[cfg(test)]
mod tests;
use noxis_analysis::{Analysis, ClassHierarchy, RenameSafety, method_rename_safety};
use noxis_classfile::Program;
use noxis_core::{Context, CoreError, Pass, PassId, PassReport, field_key, method_key};
use self::assign::{
RenameMaps, generate_packages, parent_package, rename_virtual_families, simple_name, widen_all,
};
use self::rewrite::rewrite_class;
use crate::namegen::NameGenerator;
const ACC_PUBLIC: u16 = 0x0001;
const ACC_PRIVATE: u16 = 0x0002;
const ACC_PROTECTED: u16 = 0x0004;
const ACC_STATIC: u16 = 0x0008;
const ACC_FINAL: u16 = 0x0010;
const ACC_ENUM: u16 = 0x4000;
/// Пасс переименования.
#[derive(Debug, Default, Clone, Copy)]
pub struct RenamePass;
impl Pass for RenamePass {
fn id(&self) -> PassId {
PassId::Rename
}
fn apply(&self, ctx: &mut Context, program: &mut Program) -> Result<PassReport, CoreError> {
let analysis = Analysis::run(program, &ctx.config)
.map_err(|e| CoreError::pass(PassId::Rename, e.to_string()))?;
let hierarchy = ClassHierarchy::build(program);
let owners: Vec<String> = program
.classes
.iter()
.map(|c| c.this_class.clone())
.collect();
let cfg = ctx.config.clone();
let mut maps = RenameMaps::default();
let mut renamed_classes = 0usize;
// 1. Имена классов.
{
let packages = generate_packages(&cfg, &mut ctx.rng("rename/packages"));
let mut names =
NameGenerator::new(ctx.rng("rename/classes"), cfg.rename.class_style).for_classes();
for name in &owners {
if analysis.is_locked(name) {
names.reserve(simple_name(name));
}
}
for name in &owners {
if analysis.is_locked(name) {
continue;
}
let package = if cfg.rename.spread_packages {
let idx = ctx
.rng_nth("rename/pick", renamed_classes as u64)
.below(packages.len() as u64) as usize;
packages[idx].clone()
} else {
parent_package(name)
};
let simple = names.next_name();
let new_name = if package.is_empty() {
simple
} else {
format!("{package}/{simple}")
};
maps.classes.insert(name.clone(), new_name);
renamed_classes += 1;
}
}
// 2. Простые (не-виртуальные) члены: private/static/final.
let mut renamed_methods = 0usize;
let mut renamed_fields = 0usize;
for (idx, class) in program.classes.iter().enumerate() {
let owner = class.this_class.clone();
let locked = analysis.is_locked(&owner);
let mut method_names = NameGenerator::new(
ctx.rng_nth("rename/methods", idx as u64),
cfg.rename.member_style,
);
let mut field_names = NameGenerator::new(
ctx.rng_nth("rename/fields", idx as u64),
cfg.rename.member_style,
);
if !locked {
for method in &class.methods {
if method.name == "<init>" || method.name == "<clinit>" {
method_names.reserve(method.name.clone());
continue;
}
if analysis.keep.keeps_member(&method.name) {
method_names.reserve(method.name.clone());
continue;
}
let safety = method_rename_safety(
program,
&hierarchy,
&owner,
&method.name,
&method.descriptor,
false,
);
match safety {
RenameSafety::Safe => {
let new = method_names.next_name();
maps.methods.insert(
(
owner.clone(),
method.name.clone(),
method.descriptor.clone(),
),
new,
);
renamed_methods += 1;
}
// Виртуальный не-final: переименовывается только вместе
// со всем семейством (см. rename_virtual_families).
_ => method_names.reserve(method.name.clone()),
}
}
for field in &class.fields {
if analysis.keep.keeps_member(&field.name)
|| field.name == "serialVersionUID"
|| field.name == "serialPersistentFields"
{
field_names.reserve(field.name.clone());
continue;
}
let is_enum_const = field.access_flags & ACC_ENUM != 0;
if is_enum_const && !cfg.rename.rename_enum_constants {
field_names.reserve(field.name.clone());
continue;
}
let visible = field.access_flags & (ACC_PRIVATE | ACC_STATIC | ACC_FINAL) == 0;
if visible && !cfg.rename.rename_public_members {
field_names.reserve(field.name.clone());
continue;
}
let new = field_names.next_name();
maps.fields.insert(
(owner.clone(), field.name.clone(), field.descriptor.clone()),
new,
);
renamed_fields += 1;
}
} else {
for method in &class.methods {
method_names.reserve(method.name.clone());
}
for field in &class.fields {
field_names.reserve(field.name.clone());
}
}
}
// 3. Виртуальные семейства — только в агрессивном режиме.
if cfg.rename.rename_public_members {
renamed_methods += rename_virtual_families(program, &analysis, &mut maps, &cfg, ctx);
}
// 4. Псевдоними для наследников.
maps.expand(&hierarchy, &owners);
// 5. Переписать определения и все ссылки.
let mut local_counter = 0u64;
for class in &mut program.classes {
let orig_owner = class.this_class.clone();
rewrite_class(class, &orig_owner, &maps, &cfg, &mut local_counter, ctx);
}
// 6. Ширинирование доступа: после переезда в другие пакеты иначе
// возможен IllegalAccessError.
if cfg.rename.widen_access {
widen_all(program);
}
// 7. Маппинги в отчёт (обратная деобфускация stack trace'ей).
for ((owner, name, desc), new_name) in maps.methods.iter() {
ctx.mapping
.methods
.insert(method_key(owner, name, desc), new_name.clone());
}
for ((owner, name, desc), new_name) in maps.fields.iter() {
ctx.mapping
.fields
.insert(field_key(owner, name, desc), new_name.clone());
}
for (old, new_name) in maps.classes.iter() {
ctx.mapping.classes.insert(old.clone(), new_name.clone());
}
let locked = analysis.locked_classes.len();
if locked > 0 {
ctx.note(format!("rename: {locked} классов в keep (не тронуты)"));
}
Ok(PassReport {
pass: PassId::Rename,
transformed: renamed_classes + renamed_methods + renamed_fields,
skipped: locked,
notes: vec![format!(
"классов: {renamed_classes}, методов: {renamed_methods}, полей: {renamed_fields}"
)],
})
}
}

View file

@ -0,0 +1,100 @@
//! Переписывание тела метода: инструкции, кадры, handler'ы, атрибуты кода.
use noxis_classfile::{Code, Const, Insn};
use noxis_core::{Config, Context};
use super::super::assign::{RenameMaps, rewrite_descriptor};
use super::rewrite_attributes;
use super::rewrite_frame;
pub(super) fn rewrite_code(
code: &mut Code,
maps: &RenameMaps,
cfg: &Config,
orig_owner: &str,
ctx: &mut Context,
local_counter: &mut u64,
) {
// Имена классов в кадрах тоже переименовываются, иначе верификатор
// увидит старые типы (`compute_frames` их не пересчитывает здесь).
for frame in code.frames.iter_mut() {
rewrite_frame(frame, maps);
}
for insn in code.insns.iter_mut() {
match insn {
Insn::Const(Const::Class(name)) => {
if let Some(mapped) = maps.classes.get(name.as_str()) {
*name = mapped.clone();
}
}
Insn::GetStatic {
owner,
name,
descriptor,
}
| Insn::PutStatic {
owner,
name,
descriptor,
}
| Insn::GetField {
owner,
name,
descriptor,
}
| Insn::PutField {
owner,
name,
descriptor,
} => {
if let Some(new_name) = maps.field(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);
}
Insn::Invoke {
owner,
name,
descriptor,
..
} => {
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);
}
Insn::New(name)
| Insn::CheckCast(name)
| Insn::InstanceOf(name)
| Insn::ANewArray(name) => {
if let Some(mapped) = maps.classes.get(name.as_str()) {
*name = mapped.clone();
}
}
_ => {}
}
}
for handler in code.exception_table.iter_mut() {
if let Some(catch) = handler.catch_type.clone()
&& let Some(mapped) = maps.classes.get(catch.as_str())
{
handler.catch_type = Some(mapped.clone());
}
}
rewrite_attributes(
&mut code.attributes,
maps,
cfg,
orig_owner,
ctx,
local_counter,
);
}

View file

@ -0,0 +1,173 @@
//! Переписывание одного класса: имена, дескрипторы, код, атрибуты.
mod code;
use noxis_classfile::{Attribute, ClassFile, StackMapFrame, VerificationType};
use noxis_core::{Config, Context};
use super::assign::{RenameMaps, rewrite_descriptor};
use crate::namegen::NameGenerator;
use code::rewrite_code;
pub(super) fn rewrite_class(
class: &mut ClassFile,
orig_owner: &str,
maps: &RenameMaps,
cfg: &Config,
local_counter: &mut u64,
ctx: &mut Context,
) {
if let Some(new_name) = maps.classes.get(orig_owner) {
class.this_class = new_name.clone();
}
if let Some(mapped) = class
.super_class
.as_ref()
.and_then(|sup| maps.classes.get(sup))
.cloned()
{
class.super_class = Some(mapped);
}
for iface in &mut class.interfaces {
if let Some(mapped) = maps.classes.get(iface.as_str()) {
*iface = mapped.clone();
}
}
for field in &mut class.fields {
let key = (
orig_owner.to_string(),
field.name.clone(),
field.descriptor.clone(),
);
if let Some(new_name) = maps.fields.get(&key) {
field.name = new_name.clone();
}
field.descriptor = rewrite_descriptor(&field.descriptor, &maps.classes);
rewrite_attributes(
&mut field.attributes,
maps,
cfg,
orig_owner,
ctx,
local_counter,
);
}
for method in &mut class.methods {
let key = (
orig_owner.to_string(),
method.name.clone(),
method.descriptor.clone(),
);
if let Some(new_name) = maps.methods.get(&key) {
method.name = new_name.clone();
}
method.descriptor = rewrite_descriptor(&method.descriptor, &maps.classes);
if let Some(code) = &mut method.code {
rewrite_code(code, maps, cfg, orig_owner, ctx, local_counter);
}
rewrite_attributes(
&mut method.attributes,
maps,
cfg,
orig_owner,
ctx,
local_counter,
);
}
rewrite_attributes(
&mut class.attributes,
maps,
cfg,
orig_owner,
ctx,
local_counter,
);
}
fn rewrite_attributes(
attributes: &mut [Attribute],
maps: &RenameMaps,
cfg: &Config,
orig_owner: &str,
ctx: &mut Context,
local_counter: &mut u64,
) {
for attr in attributes.iter_mut() {
match attr {
Attribute::Signature(sig) => {
*sig = rewrite_descriptor(sig, &maps.classes);
}
Attribute::Exceptions(list) => {
for name in list {
if let Some(mapped) = maps.classes.get(name.as_str()) {
*name = mapped.clone();
}
}
}
Attribute::InnerClasses(inner) => {
for entry in inner.iter_mut() {
if let Some(mapped) = maps.classes.get(entry.inner.as_str()) {
entry.inner = mapped.clone();
}
if let Some(mapped) = entry
.outer
.as_ref()
.and_then(|outer| maps.classes.get(outer))
.cloned()
{
entry.outer = Some(mapped);
}
}
}
Attribute::LocalVariableTable(vars) => {
for var in vars.iter_mut() {
var.descriptor = rewrite_descriptor(&var.descriptor, &maps.classes);
if cfg.rename.rename_locals {
*local_counter += 1;
let mut names = NameGenerator::new(
ctx.rng_nth("rename/locals", *local_counter),
cfg.rename.local_style,
);
let new = names.next_name();
ctx.mapping.locals.insert(
format!("{orig_owner}/{}@{}", var.name, var.index),
new.clone(),
);
var.name = new;
}
}
}
_ => {}
}
}
}
/// Имена классов внутри одного кадра (`Object(..)` в locals/stack).
fn rewrite_frame(frame: &mut StackMapFrame, maps: &RenameMaps) {
match frame {
StackMapFrame::Same { .. } | StackMapFrame::Chop { .. } => {}
StackMapFrame::SameLocals { local, .. } => rewrite_type(local, maps),
StackMapFrame::Append { locals, .. } => rewrite_types(locals, maps),
StackMapFrame::Full { locals, stack, .. } => {
rewrite_types(locals, maps);
rewrite_types(stack, maps);
}
}
}
fn rewrite_types(types: &mut [VerificationType], maps: &RenameMaps) {
for item in types.iter_mut() {
rewrite_type(item, maps);
}
}
fn rewrite_type(item: &mut VerificationType, maps: &RenameMaps) {
if let VerificationType::Object(name) = item
&& let Some(mapped) = maps.classes.get(name.as_str())
{
*name = mapped.clone();
}
}

View file

@ -0,0 +1,193 @@
//! Тесты пасса переименования: keep, ссылки, семейства, алиасы.
#![allow(clippy::expect_used, clippy::unwrap_used)]
use std::collections::BTreeMap;
use noxis_analysis::ClassHierarchy;
use noxis_classfile::{
ClassBuilder, ExceptionHandler, Insn, InvokeKind, Program, Resource, empty_code,
};
use noxis_core::{Config, Context, Pass, RenameConfig};
use super::RenamePass;
use super::assign::{RenameMaps, rewrite_descriptor};
fn sample() -> Program {
let main = ClassBuilder::new("app/Main", None)
.method_with_code(
0x0009,
"main",
"([Ljava/lang/String;)V",
noxis_classfile::Code {
max_stack: 4,
max_locals: 1,
insns: vec![
Insn::Invoke {
kind: InvokeKind::Static,
owner: "app/Helper".into(),
name: "make".into(),
descriptor: "()Lapp/Helper;".into(),
itf: false,
},
Insn::Pop,
Insn::Return(None),
],
exception_table: Vec::new(),
frames: Vec::new(),
attributes: Vec::new(),
},
)
.build();
let helper = ClassBuilder::new("app/Helper", None)
.method_with_code(
0x0009 | 0x0001,
"make",
"()Lapp/Helper;",
noxis_classfile::Code {
max_stack: 1,
max_locals: 0,
insns: vec![Insn::New("app/Helper".into()), Insn::Return(None)],
exception_table: Vec::new(),
frames: Vec::new(),
attributes: Vec::new(),
},
)
.method_with_code(0x0002, "secret", "()V", empty_code(0))
.build();
Program {
classes: vec![main, helper],
resources: vec![Resource {
path: "data.txt".into(),
data: b"x".to_vec(),
}],
}
}
fn run_rename(program: &mut Program) -> Context {
let config = Config {
seed: Some(9),
keep: vec!["app/Main".to_string()],
rename: RenameConfig {
package_pool: 4,
..RenameConfig::default()
},
..Config::default()
};
let mut ctx = Context::new(config);
let report = RenamePass.apply(&mut ctx, program).expect("rename");
assert!(report.transformed > 0, "ничего не переименовано");
ctx
}
#[test]
fn renames_unlocked_class_and_references() {
let mut program = sample();
let ctx = run_rename(&mut program);
assert_eq!(
program.class("app/Main").map(|c| c.this_class.as_str()),
Some("app/Main"),
"keep-класс обязан остаться"
);
let helper = program
.classes
.iter()
.find(|c| c.this_class != "app/Main")
.cloned()
.expect("helper");
assert_ne!(helper.this_class, "app/Helper");
let main = program.class("app/Main").expect("main");
let code = main.methods[0].code.as_ref().expect("code");
let (new_owner, new_desc) = match &code.insns[0] {
Insn::Invoke {
owner, descriptor, ..
} => (owner.clone(), descriptor.clone()),
other => panic!("ожидался Invoke, получено {other:?}"),
};
assert_eq!(new_owner, helper.this_class);
assert!(new_desc.contains(&helper.this_class), "desc: {new_desc}");
let renamed_secret = helper
.methods
.iter()
.find(|m| m.name != "make")
.map(|m| m.name.clone())
.expect("secret переименован");
assert_ne!(renamed_secret, "secret");
assert!(!ctx.mapping.methods.is_empty());
}
#[test]
fn descriptor_rewrite_handles_arrays() {
let mut map = BTreeMap::new();
map.insert("a/b/C".to_string(), "p/Q".to_string());
map.insert("a/b/D".to_string(), "p/R".to_string());
assert_eq!(
rewrite_descriptor("(La/b/C;[La/b/D;)La/b/C;", &map),
"(Lp/Q;[Lp/R;)Lp/Q;"
);
assert_eq!(rewrite_descriptor("([[I)V", &map), "([[I)V");
}
#[test]
fn exception_handler_type_is_rewritten() {
let mut program = sample();
program
.classes
.push(ClassBuilder::new("app/Boom", None).build());
program.classes[1].methods[0]
.code
.as_mut()
.expect("code")
.exception_table
.push(ExceptionHandler {
start: 0,
end: 1,
handler: 2,
catch_type: Some("app/Boom".into()),
});
let ctx = run_rename(&mut program);
let new_boom = ctx.mapping.class("app/Boom").expect("boom renamed");
let helper = program
.classes
.iter()
.find(|c| c.this_class != "app/Main")
.expect("helper");
let handler = &helper.methods[0]
.code
.as_ref()
.expect("code")
.exception_table[0];
assert_eq!(handler.catch_type.as_deref(), Some(new_boom));
}
#[test]
fn subclass_gets_alias_for_inherited_member() {
let base = ClassBuilder::new("p/Base", None)
.method_with_code(0x0009, "shared", "()V", empty_code(0))
.build();
let sub = ClassBuilder::new("p/Sub", Some("p/Base")).build();
let program = Program {
classes: vec![base, sub],
resources: Vec::new(),
};
let hierarchy = ClassHierarchy::build(&program);
let mut maps = RenameMaps::default();
maps.methods.insert(
("p/Base".into(), "shared".into(), "()V".into()),
"zz".into(),
);
let owners = vec!["p/Base".to_string(), "p/Sub".to_string()];
maps.expand(&hierarchy, &owners);
// Вызов через подкласс обязан находить новое имя.
assert_eq!(
maps.method("p/Base", "shared", "()V").as_deref(),
Some("zz")
);
assert_eq!(maps.method("p/Sub", "shared", "()V").as_deref(), Some("zz"));
// Чужого члена не выдумываем.
assert_eq!(maps.method("p/Sub", "other", "()V"), None);
}

View file

@ -0,0 +1,170 @@
//! Пасс ресурсов: патч текстовых файлов архива по маппингу классов.
//!
//! Без этого обфускация Fabric-мода просто не стартует: `fabric.mod.json`
//! хранит main-class точкой, `*.mixins.json` — пути внутренними именами,
//! refmap и access widener — тоже. Материал: `info/evo-weaknesses.md` §3/§8
//! (у разобранного образца ресурсы вообще не трогали).
//!
//! Замена **границная**, а не подстрочная: `org/example/Foo` не должен
//! совпасть внутри `myorg/example/FooBar`. Правило то же, что у проверенного
//! референса: сначала дескрипторный вид `Lpkg/Class;`, затем внутренний вид
//! с `/`, затем бинарный с `.`, длинные имена первыми.
/// Интеграционные тесты пасса.
#[cfg(test)]
mod tests;
use std::cmp::Reverse;
use std::collections::BTreeMap;
use noxis_classfile::Program;
use noxis_core::{Context, CoreError, Pass, PassId, PassReport};
/// Расширения текстовых ресурсов, которые имеют смысл переписывать.
const TEXT_EXTS: [&str; 9] = [
".json",
".mcmeta",
".properties",
".txt",
".xml",
".yml",
".yaml",
".cfg",
".accesswidener",
];
/// Service loader'ы: файла без расширения, но каждая строка — имя класса.
const SERVICES_PREFIX: &str = "META-INF/services/";
/// Пасс патча ресурсов.
#[derive(Debug, Default, Clone, Copy)]
pub struct ResourcesPass;
impl Pass for ResourcesPass {
fn id(&self) -> PassId {
PassId::Resources
}
fn apply(&self, ctx: &mut Context, program: &mut Program) -> Result<PassReport, CoreError> {
let map = ctx.mapping.classes.clone();
if map.is_empty() {
return Ok(PassReport {
pass: PassId::Resources,
transformed: 0,
skipped: program.resources.len(),
notes: vec!["маппинг классов пуст — ресурсы не тронуты".into()],
});
}
let mut patched = 0usize;
let mut skipped = 0usize;
for resource in &mut program.resources {
if !is_text(&resource.path) {
skipped += 1;
continue;
}
let Ok(text) = std::str::from_utf8(&resource.data) else {
skipped += 1;
continue;
};
let updated = remap_text(text, &map);
if updated == text {
skipped += 1;
continue;
}
resource.data = updated.into_bytes();
patched += 1;
}
let mut notes = Vec::new();
if patched > 0 {
notes.push(format!("патч ресурсов: {patched}, пропущено: {skipped}"));
}
Ok(PassReport {
pass: PassId::Resources,
transformed: patched,
skipped,
notes,
})
}
}
/// Текстовый ли это ресурс.
///
/// Бинарии (картинки, `.class` в `versions/`, нативные библиотеки) не
/// трогаем: их переписывание ломает содержимое молча.
pub fn is_text(path: &str) -> bool {
if path.starts_with(SERVICES_PREFIX) {
return !path.ends_with('/');
}
let lower = path.to_ascii_lowercase();
TEXT_EXTS.iter().any(|ext| lower.ends_with(ext))
}
/// Переписывает все ссылки на классы в тексте по таблице `old → new`.
pub fn remap_text(text: &str, map: &BTreeMap<String, String>) -> String {
if map.is_empty() {
return text.to_string();
}
// Длинные имена первыми: иначе `org/example/Foo` совпадёт внутри
// `org/example/FooBar` ещё до того, как до него дойдёт очередь.
let mut pairs: Vec<(&String, &String)> = map.iter().collect();
pairs.sort_by_key(|(from, _)| Reverse(from.len()));
let mut out = text.to_string();
for (from, to) in pairs {
out = out.replace(&format!("L{from};"), &format!("L{to};"));
out = replace_delimited(&out, from, to, false);
out = replace_delimited(&out, &from.replace('/', "."), &to.replace('/', "."), true);
}
out
}
/// Заменяет все вхождения `old`, отделённые от surrounding текста границами
/// имени. `dotted` выбирает набор запрещённых соседей: `/` или `.`.
fn replace_delimited(text: &str, old: &str, new: &str, dotted: bool) -> String {
let mut out = String::with_capacity(text.len());
let mut rest = text;
while let Some(at) = rest.find(old) {
let before = &rest[..at];
let after = &rest[at + old.len()..];
if is_boundary(before.chars().next_back(), dotted)
&& is_boundary(after.chars().next(), dotted)
{
out.push_str(before);
out.push_str(new);
rest = after;
continue;
}
// Совпадение частичное — пропускаем первый символ и ищем дальше.
let mut chars = rest[at..].chars();
out.push_str(before);
match chars.next() {
Some(first) => {
out.push(first);
rest = chars.as_str();
}
None => break,
}
}
out.push_str(rest);
out
}
/// `true`, если символ слева/справа допускает начало/конец имени класса.
fn is_boundary(neighbour: Option<char>, dotted: bool) -> bool {
let Some(ch) = neighbour else {
return true;
};
if ch.is_alphanumeric() || ch == '_' || ch == '$' {
return false;
}
if dotted { ch != '.' } else { ch != '/' }
}

View file

@ -0,0 +1,160 @@
//! Тесты пасса ресурсов: границы совпадений и патч содержимого.
#![allow(clippy::expect_used, clippy::unwrap_used)]
use noxis_classfile::{Program, Resource};
use noxis_core::{Config, Context, Pass};
use super::{ResourcesPass, is_text, remap_text};
fn mapping() -> std::collections::BTreeMap<String, String> {
let mut map = std::collections::BTreeMap::new();
map.insert("org/example/Foo".to_string(), "a/b/C".to_string());
map.insert("org/example/FooBar".to_string(), "a/b/D".to_string());
map
}
#[test]
fn descriptor_form_is_replaced() {
let text = "Lorg/example/Foo; [Lorg/example/FooBar;";
assert_eq!(
remap_text(text, &mapping()),
"La/b/C; [La/b/D;",
"дескрипторный вид обязан заменяться без проверки границ"
);
}
#[test]
fn internal_form_respects_boundaries() {
let map = mapping();
assert_eq!(remap_text("owner=Lorg/example/Foo;", &map), "owner=La/b/C;");
assert_eq!(
remap_text("myorg/example/Foo", &map),
"myorg/example/Foo",
"совпадение внутри более длинного пути не трогаем"
);
assert_eq!(
remap_text("org/example/FooBaz", &map),
"org/example/FooBaz",
"суффикс не должен совпасть с префиксом"
);
}
#[test]
fn dotted_form_respects_boundaries() {
let map = mapping();
assert_eq!(
remap_text("\"org.example.Foo\" => \"x\"", &map),
"\"a.b.C\" => \"x\""
);
assert_eq!(
remap_text("myorg.example.Foo", &map),
"myorg.example.Foo",
"точка внутри имени не является границей"
);
}
#[test]
fn longest_name_wins() {
assert_eq!(
remap_text("Lorg/example/FooBar;", &mapping()),
"La/b/D;",
"длинное имя обрабатывается раньше короткого"
);
}
#[test]
fn empty_map_is_a_noop() {
let text = "Lorg/example/Foo; и org.example.Foo";
assert_eq!(remap_text(text, &Default::default()), text);
}
#[test]
fn unicode_text_survives_unrelated_substitution() {
let text = "комментарий: Лорем_ипсум — org/example/Bar";
assert_eq!(remap_text(text, &mapping()), text);
}
#[test]
fn text_detection_covers_known_formats() {
assert!(is_text("fabric.mod.json"));
assert!(is_text("demo.mixins.json"));
assert!(is_text("META-INF/services/org.example.Loader"));
assert!(is_text("mod.accesswidener"));
assert!(is_text("META-INF/versions/1.20/mod.json"));
assert!(!is_text("assets/tex.png"));
assert!(!is_text("lib/native.so"));
assert!(!is_text("META-INF/services/"));
}
fn context_with_mapping() -> Context {
let mut ctx = Context::new(Config {
seed: Some(3),
..Config::default()
});
ctx.mapping
.classes
.insert("org/example/Foo".into(), "a/b/C".into());
ctx
}
#[test]
fn pass_patches_text_and_counts_the_rest() {
let mut program = Program {
classes: Vec::new(),
resources: vec![
Resource {
path: "fabric.mod.json".into(),
data: br#"{"entrypoints":{"main":["org.example.Foo"]}}"#.to_vec(),
},
Resource {
path: "META-INF/services/org.example.Foo".into(),
data: b"# keep\n".to_vec(),
},
Resource {
path: "assets/icon.png".into(),
data: vec![0x89, 0x50, 0x4E, 0x47],
},
],
};
let mut ctx = context_with_mapping();
let report = ResourcesPass
.apply(&mut ctx, &mut program)
.expect("resources");
assert_eq!(report.transformed, 1, "патчим только JSON");
assert_eq!(report.skipped, 2);
let json = std::str::from_utf8(&program.resources[0].data).unwrap();
assert!(
json.contains("a.b.C"),
"binary-имя в JSON не заменено: {json}"
);
assert!(!json.contains("org.example.Foo"), "старое имя осталось");
assert_eq!(program.resources[2].data, vec![0x89, 0x50, 0x4E, 0x47]);
}
#[test]
fn pass_is_a_noop_without_a_mapping() {
let mut program = Program {
classes: Vec::new(),
resources: vec![Resource {
path: "fabric.mod.json".into(),
data: br#"{"x":"org.example.Foo"}"#.to_vec(),
}],
};
let mut ctx = Context::new(Config::default());
let report = ResourcesPass
.apply(&mut ctx, &mut program)
.expect("resources");
assert_eq!(report.transformed, 0);
assert_eq!(report.skipped, 1);
assert!(
!report.notes.is_empty(),
"должна быть заметка о пустом маппинге"
);
}

View file

@ -0,0 +1,60 @@
//! Шифрование строк: чанки, salt под схему, последовательность вызова.
//!
//! Один `Const::String` превращается в `ldc cipher; ldc salt; invokestatic
//! d` на чанк; чанки склеиваются `String.concat` (без `StringBuilder` —
//! меньше инструкций и нет вызовов конструктора).
use noxis_classfile::{Const, Insn, InvokeKind};
use noxis_runtime::{DECODER_DESC, DECODER_METHOD};
/// Разбить строку на чанки по границе символов (1–3 штуки).
pub(super) fn split_chunks(text: &str, split_above: usize) -> Vec<String> {
let chars: Vec<char> = text.chars().collect();
if chars.len() <= split_above.max(1) {
return vec![text.to_string()];
}
let count = chars.len().div_ceil(split_above);
let count = count.clamp(1, 3);
let per = chars.len().div_ceil(count);
chars
.chunks(per)
.map(|chunk| chunk.iter().collect())
.collect()
}
/// Salt, попадающий в нужную схему: `salt % total == index`.
///
/// Salt всегда неотрицательный (Java `%` сохраняет знак — отрицательный salt
/// выбрал бы не ту ветку декодера). `index` обязан быть меньше `total`.
pub(super) fn salt_for_scheme(raw: u64, index: usize, total: usize) -> i32 {
let total = total.max(1) as u64;
let index = (index as u64) % total;
let rounds = (i32::MAX as u64 - index) / total + 1;
(index + (raw >> 33) % rounds * total) as i32
}
/// Вызовы декодера на чанки + склейка (`[(cipher, salt)]`).
pub(super) fn decode_sequence(owner: &str, parts: &[(String, i32)]) -> Vec<Insn> {
let mut out = Vec::with_capacity(parts.len() * 4);
for (position, (cipher, salt)) in parts.iter().enumerate() {
out.push(Insn::Const(Const::String(cipher.clone())));
out.push(Insn::Const(Const::Int(*salt)));
out.push(Insn::Invoke {
kind: InvokeKind::Static,
owner: owner.to_string(),
name: DECODER_METHOD.to_string(),
descriptor: DECODER_DESC.to_string(),
itf: false,
});
if position > 0 {
out.push(Insn::Invoke {
kind: InvokeKind::Virtual,
owner: "java/lang/String".to_string(),
name: "concat".to_string(),
descriptor: "(Ljava/lang/String;)Ljava/lang/String;".to_string(),
itf: false,
});
}
}
out
}

View file

@ -0,0 +1,193 @@
//! Пасс шифрования строк: `Const::String` → вызовы рантайм-декодера.
//!
//! Схема строки выбирается ротацией (`salt % n`), salt детерминирован
//! (KDF или RNG-поток — см. `derive_key`). Декодер генерируется один на
//! программу и только если была зашифрована хотя бы одна строка.
mod crypt;
#[cfg(test)]
mod tests;
use std::collections::BTreeMap;
use noxis_analysis::ClassHierarchy;
use noxis_classfile::{Const, Insn, Program, compute_frames};
use noxis_core::{Context, CoreError, NameStyle, Pass, PassId, PassReport, derive_seed};
use noxis_runtime::{Scheme, decoder_class, decoder_owner, encrypt};
use crate::namegen::NameGenerator;
use crypt::{decode_sequence, salt_for_scheme, split_chunks};
/// Пасс шифрования строк.
#[derive(Debug, Default, Clone, Copy)]
pub struct StringsPass;
impl Pass for StringsPass {
fn id(&self) -> PassId {
PassId::Strings
}
fn apply(&self, ctx: &mut Context, program: &mut Program) -> Result<PassReport, CoreError> {
let cfg = ctx.config.strings.clone();
let mut schemes = Vec::with_capacity(cfg.schemes.len());
for name in &cfg.schemes {
schemes.push(Scheme::parse(name).map_err(|e| pass_error(e.to_string()))?);
}
if schemes.is_empty() {
return Err(pass_error("нет схем шифрования"));
}
// Фаза 1: собрать все шифруемые строки (позиция + текст).
let mut targets: Vec<(usize, usize, usize, String)> = Vec::new();
let mut skipped = 0usize;
for (ci, class) in program.classes.iter().enumerate() {
for (mi, method) in class.methods.iter().enumerate() {
let Some(code) = method.code.as_ref() else {
continue;
};
for (ii, insn) in code.insns.iter().enumerate() {
if let Insn::Const(Const::String(text)) = insn {
if text.chars().count() >= cfg.min_len && !is_skipped(text, &cfg.skip) {
targets.push((ci, mi, ii, text.clone()));
} else {
skipped += 1;
}
}
}
}
}
if targets.is_empty() {
return Ok(report(
0,
skipped,
vec!["нет строк для шифрования".to_string()],
));
}
// Фаза 2: зашифровать и подставить последовательности вызовов.
let master = ctx.master_seed;
let mut rng = ctx.rng("strings");
let owner = decoder_name(ctx, program);
let mut chunk_counter = 0usize;
let mut salt_counter = 0u64;
let mut replacements: BTreeMap<(usize, usize, usize), Vec<Insn>> = BTreeMap::new();
for (ci, mi, ii, text) in &targets {
let mut parts = Vec::new();
for chunk in split_chunks(text, cfg.split_above) {
let scheme = chunk_counter % schemes.len();
let raw = next_salt(&mut rng, master, &mut salt_counter, cfg.derive_key);
let salt = salt_for_scheme(raw, scheme, schemes.len());
let blob = encrypt(schemes[scheme], master, salt, &chunk);
parts.push((blob.cipher, salt));
chunk_counter += 1;
}
replacements.insert((*ci, *mi, *ii), decode_sequence(&owner, &parts));
}
let mut mutated: Vec<(usize, usize)> = Vec::new();
for (ci, class) in program.classes.iter_mut().enumerate() {
for (mi, method) in class.methods.iter_mut().enumerate() {
let Some(code) = method.code.as_mut() else {
continue;
};
let mut fresh = Vec::with_capacity(code.insns.len());
let mut touched = false;
for (ii, insn) in code.insns.drain(..).enumerate() {
if let Some(sequence) = replacements.remove(&(ci, mi, ii)) {
fresh.extend(sequence);
touched = true;
} else {
fresh.push(insn);
}
}
if touched {
code.max_stack = code.max_stack.saturating_add(2);
mutated.push((ci, mi));
}
code.insns = fresh;
}
}
// Фаза 3: класс-декодер один на программу.
let scheme_names: Vec<&str> = cfg.schemes.iter().map(String::as_str).collect();
let decoder =
decoder_class(&owner, master, &scheme_names).map_err(|e| pass_error(e.to_string()))?;
program.classes.push(decoder);
ctx.note(format!("декодер строк: {owner}"));
// Фаза 4: фреймы затронутых методов.
let hierarchy = ClassHierarchy::build(program);
for (ci, mi) in &mutated {
let (name, snapshot) = {
let class = program
.classes
.get(*ci)
.ok_or_else(|| pass_error("класс пропал"))?;
(
class.this_class.clone(),
class
.methods
.get(*mi)
.cloned()
.ok_or_else(|| pass_error("метод пропал"))?,
)
};
let frames = compute_frames(&name, &snapshot, &hierarchy)
.map_err(|e| pass_error(e.to_string()))?;
program
.classes
.get_mut(*ci)
.and_then(|c| c.methods.get_mut(*mi))
.and_then(|m| m.code.as_mut())
.ok_or_else(|| pass_error("метод пропал"))?
.frames = frames;
}
Ok(report(
targets.len(),
skipped,
vec![format!(
"зашифровано строк: {}, декодер: {owner}",
targets.len()
)],
))
}
}
fn report(transformed: usize, skipped: usize, notes: Vec<String>) -> PassReport {
PassReport {
pass: PassId::Strings,
transformed,
skipped,
notes,
}
}
fn pass_error(msg: impl Into<String>) -> CoreError {
CoreError::pass(PassId::Strings, msg)
}
/// Строка подпадает под `skip`-паттерны конфига.
fn is_skipped(text: &str, skip: &[String]) -> bool {
skip.iter().any(|pattern| text.contains(pattern))
}
/// Salt чанка: KDF от позиции или RNG-поток (см. `derive_key`).
fn next_salt(rng: &mut noxis_core::Rng, master: u64, counter: &mut u64, derive: bool) -> u64 {
let value = if derive {
derive_seed(master, "strings/salt", *counter)
} else {
rng.next_u64()
};
*counter += 1;
value
}
/// Детерминированное имя декодера без коллизий с классами программы.
fn decoder_name(ctx: &mut Context, program: &Program) -> String {
let mut names = NameGenerator::new(ctx.rng("strings/decoder"), NameStyle::ShortLatin);
for class in &program.classes {
names.reserve(class.this_class.clone());
}
loop {
let owner = decoder_owner(&format!("{}/{}", names.next_name(), names.next_name()));
if program.class(&owner).is_none() {
return owner;
}
}
}

View file

@ -0,0 +1,129 @@
//! Тесты пасса строк: подстановка, чанки, детерминизм шифротекста.
#![allow(clippy::expect_used, clippy::unwrap_used)]
use noxis_classfile::{ClassBuilder, Code, Const, Insn, Program, Version};
use noxis_core::{Config, Context};
use noxis_runtime::{Scheme, encrypt};
use super::StringsPass;
use super::crypt::{decode_sequence, salt_for_scheme, split_chunks};
use noxis_core::Pass;
fn sample() -> Program {
let code = Code {
max_stack: 1,
max_locals: 0,
insns: vec![
Insn::Const(Const::String("hello world".to_string())),
Insn::Const(Const::String("x".to_string())),
Insn::Const(Const::String("y".repeat(100))),
Insn::Const(Const::Int(5)),
Insn::Return(None),
],
exception_table: Vec::new(),
frames: Vec::new(),
attributes: Vec::new(),
};
Program {
classes: vec![
ClassBuilder::new("demo/App", Some("java/lang/Object"))
.version(Version::JAVA_21)
.method_with_code(0x0009, "run", "()V", code)
.build(),
],
resources: Vec::new(),
}
}
/// Короткие строки не режутся, длинные — на границе символов.
#[test]
fn chunks_split_by_chars() {
assert_eq!(split_chunks("abc", 64), vec!["abc".to_string()]);
let chunks = split_chunks(&"y".repeat(100), 64);
assert_eq!(chunks.len(), 2);
assert_eq!(chunks.concat().len(), 100);
}
/// Salt всегда неотрицателен и попадает в схему.
#[test]
fn salt_hits_scheme() {
for raw in [0u64, 1, 7, u64::MAX, 123456789] {
for (index, total) in [(0, 1), (0, 2), (1, 2), (2, 3)] {
let salt = salt_for_scheme(raw, index, total);
assert!(salt >= 0, "отрицательный salt");
assert_eq!(salt as usize % total, index);
}
}
}
/// Пасс: строки заменены вызовами, шифротекст воспроизводим, декодер есть.
#[test]
fn strings_replaced_and_deterministic() {
let mut program = sample();
let config = Config {
seed: Some(7),
..Config::default()
};
let mut ctx = Context::new(config.clone());
let report = StringsPass.apply(&mut ctx, &mut program).expect("strings");
assert_eq!(report.transformed, 2);
assert_eq!(report.skipped, 1);
// Декодер добавлен последним.
assert_eq!(program.classes.len(), 2);
let decoder = program.classes.last().expect("декодер");
assert!(decoder.methods.iter().any(|m| m.name == "d"));
// Исходных строк не осталось (шифры — длинные Const::String — на месте);
// короткая на месте.
let code = program.classes[0].methods[0].code.as_ref().expect("код");
let kept: Vec<&String> = code
.insns
.iter()
.filter_map(|insn| match insn {
Insn::Const(Const::String(s)) => Some(s),
_ => None,
})
.collect();
assert!(!kept.contains(&&"hello world".to_string()));
assert!(!kept.iter().any(|s| *s == &"y".repeat(100)));
assert!(kept.contains(&&"x".to_string()));
// Каждый вызов сверяется с прямым шифрованием (тот же сид → те же блобы).
let schemes = [Scheme::XorSplitmix, Scheme::XteaCtr];
let mut triples = Vec::new();
let mut window: Vec<&Insn> = Vec::new();
for insn in &code.insns {
window.push(insn);
if window.len() > 3 {
window.remove(0);
}
if let [
Insn::Const(Const::String(cipher)),
Insn::Const(Const::Int(salt)),
Insn::Invoke { name, .. },
] = &window[..]
&& name == "d"
{
triples.push((cipher.clone(), *salt));
}
}
// "hello world" (1 чанк) + "y"*100 (2 чанка).
assert_eq!(triples.len(), 3);
let originals = ["hello world".to_string(), "y".repeat(50), "y".repeat(50)];
for ((cipher, salt), original) in triples.iter().zip(originals) {
let scheme = schemes[*salt as usize % schemes.len()];
let blob = encrypt(scheme, 7, *salt, &original);
assert_eq!(cipher, &blob.cipher);
}
// Склейка чанков через `concat` — ровно одна; конструктор жив.
assert_eq!(
code.insns
.iter()
.filter(|insn| matches!(insn, Insn::Invoke { name, .. } if name == "concat"))
.count(),
1
);
assert!(!decode_sequence("o", &[("c".to_string(), 1)]).is_empty());
}

View file

@ -0,0 +1,18 @@
[package]
name = "noxis-runtime"
description = "Схемы шифрования строк и генерация рантайм-декодеров для JVM"
version.workspace = true
edition.workspace = true
rust-version.workspace = true
license.workspace = true
[dependencies]
noxis-classfile = { workspace = true }
noxis-core = { workspace = true }
thiserror = { workspace = true }
[lints]
workspace = true
[build-dependencies]
noxis-structure-check = { workspace = true }

View file

@ -0,0 +1,3 @@
fn main() {
noxis_structure_check::check();
}

View file

@ -0,0 +1,108 @@
//! Кодирование шифротекста в вид, допустимый для `CONSTANT_String`.
//!
//! LDC работает только со строками (modified UTF-8), поэтому байты шифротекста
//! кодируем ASCII-алфавитом: без Base64 из JDK и без лишних зависимостей.
const ALPHABET: &[u8; 64] = b"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
/// Ошибка кодирования.
#[derive(Debug, thiserror::Error, PartialEq, Eq)]
pub enum CodecError {
#[error("некорректный base64")]
InvalidBase64,
}
/// Base64 (стандартный алфавит, без переносов строк).
pub fn encode(data: &[u8]) -> String {
let mut out = String::with_capacity(data.len().div_ceil(3) * 4);
for chunk in data.chunks(3) {
let b0 = chunk[0] as u32;
let b1 = *chunk.get(1).unwrap_or(&0) as u32;
let b2 = *chunk.get(2).unwrap_or(&0) as u32;
let triple = (b0 << 16) | (b1 << 8) | b2;
out.push(ALPHABET[(triple >> 18) as usize & 63] as char);
out.push(ALPHABET[(triple >> 12) as usize & 63] as char);
if chunk.len() > 1 {
out.push(ALPHABET[(triple >> 6) as usize & 63] as char);
} else {
out.push('=');
}
if chunk.len() > 2 {
out.push(ALPHABET[triple as usize & 63] as char);
} else {
out.push('=');
}
}
out
}
fn val(c: u8) -> Option<u32> {
match c {
b'A'..=b'Z' => Some(u32::from(c - b'A')),
b'a'..=b'z' => Some(u32::from(c - b'a') + 26),
b'0'..=b'9' => Some(u32::from(c - b'0') + 52),
b'+' => Some(62),
b'/' => Some(63),
_ => None,
}
}
/// Base64 → байты.
pub fn decode(text: &str) -> Result<Vec<u8>, CodecError> {
let bytes = text.as_bytes();
if !bytes.len().is_multiple_of(4) {
return Err(CodecError::InvalidBase64);
}
let mut out = Vec::with_capacity(bytes.len() / 4 * 3);
for chunk in bytes.chunks(4) {
let mut acc = 0u32;
let mut pads = 0;
for (i, &c) in chunk.iter().enumerate() {
if c == b'=' {
if i < 2 {
return Err(CodecError::InvalidBase64);
}
pads += 1;
acc <<= 6;
continue;
}
let v = val(c).ok_or(CodecError::InvalidBase64)?;
acc = (acc << 6) | v;
}
out.push((acc >> 16) as u8);
if pads < 2 {
out.push((acc >> 8) as u8);
}
if pads < 1 {
out.push(acc as u8);
}
}
Ok(out)
}
#[cfg(test)]
mod tests {
#![allow(clippy::expect_used, clippy::unwrap_used)]
use super::*;
#[test]
fn roundtrip() {
for data in [
&b""[..],
b"a",
b"ab",
b"abc",
b"abcd",
&[0u8, 255, 128, 7][..],
] {
let enc = encode(data);
assert_eq!(decode(&enc).expect("decode"), data, "data={data:?}");
}
}
#[test]
fn rejects_garbage() {
assert!(decode("abc").is_err());
assert!(decode("!!!!").is_err());
}
}

View file

@ -0,0 +1,196 @@
//! KDF декодера: FNV-1a64 поверх `master || domain || salt`.
//!
//! Зеркало `noxis_core::derive_seed` (тот же FNV-1a64, те же домены).
//! Вход собирается `ByteBuffer`, чтобы не материализовывать 26 байт
//! поштучно: `allocate.putLong(master).put(domain).putLong(salt).array()`.
//!
//! Здесь же — строитель тела метода и короткие эмиссии (общие для всех
//! ветвей декодера).
use noxis_classfile::{Arith, BranchCond, Cond, Const, Insn, InvokeKind, Label, Prim, SlotKind};
/// Строитель тела метода: метки, слоты, короткие эмиссии.
pub(super) struct Gen {
pub insns: Vec<Insn>,
labels: u32,
pub slots: u16,
}
impl Gen {
pub(super) fn new() -> Self {
Self {
insns: Vec::new(),
labels: 0,
// Слоты 0 (`cipher`) и 1 (`salt`) — параметры метода.
slots: 2,
}
}
pub(super) fn label(&mut self) -> Label {
let label = self.labels;
self.labels += 1;
label
}
/// Занять слоты (`wide` — пара под `long`).
pub(super) fn alloc(&mut self, wide: bool) -> u16 {
let slot = self.slots;
self.slots += if wide { 2 } else { 1 };
slot
}
pub(super) fn emit(&mut self, insn: Insn) {
self.insns.push(insn);
}
pub(super) fn mark(&mut self, label: Label) {
self.emit(Insn::Mark(label));
}
}
/// Короткие эмиссии операндов (`writer` выбирает `iconst`/`bipush` сам).
pub(super) fn c_int(value: i32) -> Insn {
Insn::Const(Const::Int(value))
}
pub(super) fn c_long(value: i64) -> Insn {
Insn::Const(Const::Long(value))
}
pub(super) fn c_str(value: &str) -> Insn {
Insn::Const(Const::String(value.to_string()))
}
pub(super) fn load(slot: u16, kind: SlotKind) -> Insn {
Insn::Load { slot, kind }
}
pub(super) fn store(slot: u16, kind: SlotKind) -> Insn {
Insn::Store { slot, kind }
}
pub(super) fn call(owner: &str, name: &str, descriptor: &str, kind: InvokeKind) -> Insn {
Insn::Invoke {
kind,
owner: owner.to_string(),
name: name.to_string(),
descriptor: descriptor.to_string(),
itf: false,
}
}
/// FNV-1a64 init (знаковое представление для `ldc2_w`).
const FNV_INIT: i64 = 0xCBF2_9CE4_8422_2325u64 as i64;
/// FNV-1a64 prime.
const FNV_PRIME: i64 = 0x0000_0100_0000_01B3;
/// Вычислить `derive_seed(master, domain, salt)`; вернуть слот пары.
pub(super) fn emit_kdf(asm: &mut Gen, master: u16, domain: &str) -> u16 {
let input = asm.alloc(false);
build_input(asm, master, domain, input);
let hash = asm.alloc(true);
let index = asm.alloc(false);
asm.emit(c_long(FNV_INIT));
asm.emit(store(hash, SlotKind::Long));
asm.emit(c_int(0));
asm.emit(store(index, SlotKind::Int));
let again = asm.label();
let done = asm.label();
asm.mark(again);
asm.emit(load(index, SlotKind::Int));
asm.emit(load(input, SlotKind::Ref));
asm.emit(Insn::ArrayLength);
asm.emit(Insn::Branch(BranchCond::Int(Cond::Ge), done));
asm.emit(load(hash, SlotKind::Long));
asm.emit(load(input, SlotKind::Ref));
asm.emit(load(index, SlotKind::Int));
asm.emit(Insn::ArrayLoad(noxis_classfile::ArrayKind::Byte));
asm.emit(Insn::Convert(noxis_classfile::Convert::I2L));
asm.emit(c_long(0xFF));
asm.emit(Insn::Binary {
prim: Prim::Long,
op: Arith::And,
});
asm.emit(Insn::Binary {
prim: Prim::Long,
op: Arith::Xor,
});
asm.emit(store(hash, SlotKind::Long));
asm.emit(load(hash, SlotKind::Long));
asm.emit(c_long(FNV_PRIME));
asm.emit(Insn::Binary {
prim: Prim::Long,
op: Arith::Mul,
});
asm.emit(store(hash, SlotKind::Long));
asm.emit(Insn::Iinc { index, delta: 1 });
asm.emit(Insn::Goto(again));
asm.mark(done);
let out = asm.alloc(true);
asm.emit(load(hash, SlotKind::Long));
asm.emit(store(out, SlotKind::Long));
out
}
/// Собрать вход `master || domain || salt` в слот `input`.
///
/// Rust считает FNV по little-endian байтам, а `putLong` кладёт big-endian —
/// поэтому оба `long` разворачиваются (`Long.reverseBytes`).
fn build_input(asm: &mut Gen, master: u16, domain: &str, input: u16) {
asm.emit(c_int(16 + domain.len() as i32));
asm.emit(call(
"java/nio/ByteBuffer",
"allocate",
"(I)Ljava/nio/ByteBuffer;",
InvokeKind::Static,
));
asm.emit(Insn::Dup);
asm.emit(load(master, SlotKind::Long));
asm.emit(call(
"java/lang/Long",
"reverseBytes",
"(J)J",
InvokeKind::Static,
));
asm.emit(call(
"java/nio/ByteBuffer",
"putLong",
"(J)Ljava/nio/ByteBuffer;",
InvokeKind::Virtual,
));
asm.emit(Insn::Dup);
asm.emit(c_str(domain));
asm.emit(call(
"java/lang/String",
"getBytes",
"()[B",
InvokeKind::Virtual,
));
asm.emit(call(
"java/nio/ByteBuffer",
"put",
"([B)Ljava/nio/ByteBuffer;",
InvokeKind::Virtual,
));
asm.emit(load(1, SlotKind::Int));
asm.emit(Insn::Convert(noxis_classfile::Convert::I2L));
asm.emit(call(
"java/lang/Long",
"reverseBytes",
"(J)J",
InvokeKind::Static,
));
asm.emit(call(
"java/nio/ByteBuffer",
"putLong",
"(J)Ljava/nio/ByteBuffer;",
InvokeKind::Virtual,
));
asm.emit(call(
"java/nio/ByteBuffer",
"array",
"()[B",
InvokeKind::Virtual,
));
asm.emit(store(input, SlotKind::Ref));
}

View file

@ -0,0 +1,186 @@
//! Рантайм-декодер: ABI, который получает байткод после подстановки строк.
//!
//! ABI v1 (стабильный, его же нужно повторить на Java side):
//!
//! ```text
//! static String d(String cipher, int salt)
//! ```
//!
//! - `cipher` — base64 шифротекста (схема выбирается по salt: `scheme = all[salt % n]`);
//! - `salt` — индекс строки, по нему детерминированно выводятся key и nonce;
//! - мастер-секрет **не передаётся на вызове** и не лежит в одной константе:
//! он собирается из двух частей (`k1 ^ k2`) прямо в декодере.
//!
//! Класс версии 49: циклы верифицируются выводом типов без `StackMapTable`
//! (проверено загрузкой v49 на Java 21), кадры не генерируются.
mod kdf;
mod xor;
mod xtea;
use noxis_classfile::{
Arith, BranchCond, ClassBuilder, ClassFile, Code, Cond, Insn, InvokeKind, Prim, SlotKind,
Version,
};
use crate::scheme::Scheme;
use kdf::{Gen, c_int, c_long, c_str, call, load, store};
/// Имя метода декодера.
pub const DECODER_METHOD: &str = "d";
/// Дескриптор метода декодера.
pub const DECODER_DESC: &str = "(Ljava/lang/String;I)Ljava/lang/String;";
/// Ошибка генерации рантайма.
#[derive(Debug, thiserror::Error)]
pub enum RuntimeError {
#[error("не реализовано: {0}")]
Unimplemented(&'static str),
#[error("нет активных схем шифрования")]
NoSchemes,
}
/// Сгенерировать класс-декодер (мастер-секрет + расшифровка всех схем).
pub fn decoder_class(
internal_name: &str,
master: u64,
schemes: &[&str],
) -> Result<ClassFile, RuntimeError> {
if schemes.is_empty() {
return Err(RuntimeError::NoSchemes);
}
let mut parsed = Vec::with_capacity(schemes.len());
for name in schemes {
parsed.push(Scheme::parse(name).map_err(|_| RuntimeError::NoSchemes)?);
}
let mut asm = Gen::new();
let master_slot = emit_master(&mut asm, master);
let key = kdf::emit_kdf(&mut asm, master_slot, "string/key");
let nonce = kdf::emit_kdf(&mut asm, master_slot, "string/nonce");
let data = emit_base64(&mut asm);
let len = emit_len(&mut asm, data);
emit_dispatch(&mut asm, &parsed, key, nonce, data, len);
emit_epilogue(&mut asm, data);
let code = Code {
// Точный пик — 10 слотов (замер 08.10.2026); запас без вреда:
// лимит обязан лишь покрывать фактическую глубину.
max_stack: 12,
max_locals: asm.slots,
insns: asm.insns,
exception_table: Vec::new(),
frames: Vec::new(),
attributes: Vec::new(),
};
Ok(ClassBuilder::new(internal_name.to_string(), None)
.version(Version {
minor: 0,
major: 49,
})
.method_with_code(0x0009, DECODER_METHOD, DECODER_DESC, code)
.build())
}
/// Имя класса-декодера для прогона: детерминированно от сида, но выглядит
/// как обычный обфусцированный класс.
pub fn decoder_owner(seed_name: &str) -> String {
seed_name.to_string()
}
/// Пролог: `master = k1 ^ k2`; возвращает слот пары. Ни одна часть не равна
/// мастер-секрету (вторая — `master ^ k1`, первая — потоковая).
fn emit_master(asm: &mut Gen, master: u64) -> u16 {
use noxis_core::Rng;
let mut rng = Rng::from_seed(master ^ 0x9E37_79B9_7F4A_7C15);
let first = rng.next_u64() as i64;
let second = (master ^ first as u64) as i64;
let slot = asm.alloc(true);
asm.emit(c_long(first));
asm.emit(c_long(second));
asm.emit(Insn::Binary {
prim: Prim::Long,
op: Arith::Xor,
});
asm.emit(store(slot, SlotKind::Long));
slot
}
/// `data = Base64.getDecoder().decode(cipher)`; возвращает слот массива.
fn emit_base64(asm: &mut Gen) -> u16 {
let slot = asm.alloc(false);
asm.emit(call(
"java/util/Base64",
"getDecoder",
"()Ljava/util/Base64$Decoder;",
InvokeKind::Static,
));
asm.emit(load(0, SlotKind::Ref));
asm.emit(call(
"java/util/Base64$Decoder",
"decode",
"(Ljava/lang/String;)[B",
InvokeKind::Virtual,
));
asm.emit(store(slot, SlotKind::Ref));
slot
}
/// `len = data.length`; возвращает слот.
fn emit_len(asm: &mut Gen, data: u16) -> u16 {
let slot = asm.alloc(false);
asm.emit(load(data, SlotKind::Ref));
asm.emit(Insn::ArrayLength);
asm.emit(store(slot, SlotKind::Int));
slot
}
/// Диспетчер схем: `salt % n` лестницей `if_icmpne` (n == 1 — без диспетчера).
fn emit_dispatch(asm: &mut Gen, schemes: &[Scheme], key: u16, nonce: u16, data: u16, len: u16) {
if schemes.len() == 1 {
dispatch_branch(asm, schemes[0], key, nonce, data, len);
return;
}
let done = asm.label();
for (index, scheme) in schemes.iter().enumerate() {
if index + 1 == schemes.len() {
break;
}
let next = asm.label();
asm.emit(load(1, SlotKind::Int));
asm.emit(c_int(schemes.len() as i32));
asm.emit(Insn::Binary {
prim: Prim::Int,
op: Arith::Rem,
});
asm.emit(c_int(index as i32));
asm.emit(Insn::Branch(BranchCond::Int(Cond::Ne), next));
dispatch_branch(asm, *scheme, key, nonce, data, len);
asm.emit(Insn::Goto(done));
asm.mark(next);
}
dispatch_branch(asm, schemes[schemes.len() - 1], key, nonce, data, len);
asm.mark(done);
}
fn dispatch_branch(asm: &mut Gen, scheme: Scheme, key: u16, nonce: u16, data: u16, len: u16) {
match scheme {
Scheme::XorSplitmix => xor::emit_xor(asm, key, nonce, data, len),
Scheme::XteaCtr => xtea::emit_xtea(asm, key, nonce, data, len),
}
}
/// Эпилог: `return new String(data, "UTF-8")`.
fn emit_epilogue(asm: &mut Gen, data: u16) {
asm.emit(Insn::New("java/lang/String".to_string()));
asm.emit(Insn::Dup);
asm.emit(load(data, SlotKind::Ref));
asm.emit(c_str("UTF-8"));
asm.emit(call(
"java/lang/String",
"<init>",
"([BLjava/lang/String;)V",
InvokeKind::Special,
));
asm.emit(Insn::AReturn);
}

View file

@ -0,0 +1,243 @@
//! Ветвь `xor-splitmix`: keystream из xoshiro256++ поверх SplitMix-сида.
//!
//! Зеркало `scheme::xor_splitmix` + `Rng::{from_seed, next_u64}` побитово:
//! сид `key ^ rotl(nonce, 17)`, стрим LE-байтами `next_u64`.
use noxis_classfile::{Arith, ArrayKind, BranchCond, Cond, Convert, Insn, Prim, SlotKind};
use super::kdf::{Gen, c_int, c_long, load, store};
/// Расшифровать `data` на месте; `len` — длина массива.
pub(super) fn emit_xor(asm: &mut Gen, key: u16, nonce: u16, data: u16, len: u16) {
let liga = Slots::alloc(asm);
emit_seed(asm, key, nonce, &liga);
emit_states(asm, &liga);
emit_loop(asm, data, len, &liga);
}
/// Слоты ветви (выделяются один раз, используются последовательно).
struct Slots {
seed: u16,
state: [u16; 4],
stream: u16,
index: u16,
inner: u16,
byte: u16,
tmp_a: u16,
tmp_b: u16,
tmp_c: u16,
}
impl Slots {
fn alloc(asm: &mut Gen) -> Self {
Self {
seed: asm.alloc(true),
state: [
asm.alloc(true),
asm.alloc(true),
asm.alloc(true),
asm.alloc(true),
],
stream: asm.alloc(true),
index: asm.alloc(false),
inner: asm.alloc(false),
byte: asm.alloc(false),
tmp_a: asm.alloc(true),
tmp_b: asm.alloc(true),
tmp_c: asm.alloc(true),
}
}
}
/// `seed = key ^ rotl(nonce, 17)`.
fn emit_seed(asm: &mut Gen, key: u16, nonce: u16, slots: &Slots) {
asm.emit(load(nonce, SlotKind::Long));
asm.emit(store(slots.tmp_a, SlotKind::Long));
asm.emit(load(slots.tmp_a, SlotKind::Long));
asm.emit(c_int(17));
asm.emit(shift(Arith::Shl));
asm.emit(store(slots.tmp_b, SlotKind::Long));
asm.emit(load(slots.tmp_a, SlotKind::Long));
asm.emit(c_int(47));
asm.emit(shift(Arith::Ushr));
asm.emit(load(slots.tmp_b, SlotKind::Long));
asm.emit(long_op(Arith::Or));
asm.emit(load(key, SlotKind::Long));
asm.emit(long_op(Arith::Xor));
asm.emit(store(slots.seed, SlotKind::Long));
}
/// `s[i] = splitmix(seed)` четыре раза.
fn emit_states(asm: &mut Gen, slots: &Slots) {
asm.emit(load(slots.seed, SlotKind::Long));
asm.emit(store(slots.tmp_a, SlotKind::Long));
for state in slots.state {
splitmix(asm, slots.tmp_a, state);
}
}
/// Один `splitmix64`: `sm += PHI`, финализатор — в `out`.
fn splitmix(asm: &mut Gen, sm: u16, out: u16) {
const PHI: i64 = 0x9E37_79B9_7F4A_7C15u64 as i64;
const C1: i64 = 0xBF58_476D_1CE4_E5B9u64 as i64;
const C2: i64 = 0x94D0_49BB_1331_11EBu64 as i64;
asm.emit(load(sm, SlotKind::Long));
asm.emit(c_long(PHI));
asm.emit(long_op(Arith::Add));
asm.emit(Insn::Dup2);
asm.emit(store(sm, SlotKind::Long));
finalize(asm, 30, C1);
finalize(asm, 27, C2);
asm.emit(Insn::Dup2);
asm.emit(c_int(31));
asm.emit(shift(Arith::Ushr));
asm.emit(long_op(Arith::Xor));
asm.emit(store(out, SlotKind::Long));
}
/// `(z ^ (z >>> amount)) * c` для вершины стека (результат остаётся наверху).
fn finalize(asm: &mut Gen, amount: i32, constant: i64) {
asm.emit(Insn::Dup2);
asm.emit(c_int(amount));
asm.emit(shift(Arith::Ushr));
asm.emit(long_op(Arith::Xor));
asm.emit(c_long(constant));
asm.emit(long_op(Arith::Mul));
}
/// Цикл по чанкам: свежий `next_u64` на 8 байт, внутренний — по байтам.
///
/// Без `skip`-джойна: стрим всегда свеж на входе во внутренний цикл.
fn emit_loop(asm: &mut Gen, data: u16, len: u16, slots: &Slots) {
asm.emit(c_int(0));
asm.emit(store(slots.index, SlotKind::Int));
let outer = asm.label();
let outer_done = asm.label();
asm.mark(outer);
asm.emit(load(slots.index, SlotKind::Int));
asm.emit(load(len, SlotKind::Int));
asm.emit(Insn::Branch(BranchCond::Int(Cond::Ge), outer_done));
emit_next(asm, slots);
asm.emit(c_int(0));
asm.emit(store(slots.inner, SlotKind::Int));
let inner = asm.label();
let inner_done = asm.label();
asm.mark(inner);
asm.emit(load(slots.inner, SlotKind::Int));
asm.emit(c_int(8));
asm.emit(Insn::Branch(BranchCond::Int(Cond::Ge), inner_done));
asm.emit(load(slots.index, SlotKind::Int));
asm.emit(load(slots.inner, SlotKind::Int));
asm.emit(int_op(Arith::Add));
asm.emit(load(len, SlotKind::Int));
asm.emit(Insn::Branch(BranchCond::Int(Cond::Ge), inner_done));
asm.emit(load(slots.stream, SlotKind::Long));
asm.emit(load(slots.inner, SlotKind::Int));
asm.emit(c_int(3));
asm.emit(int_op(Arith::Shl));
asm.emit(shift(Arith::Ushr));
asm.emit(Insn::Convert(Convert::L2I));
asm.emit(c_int(0xFF));
asm.emit(int_op(Arith::And));
asm.emit(store(slots.byte, SlotKind::Int));
asm.emit(load(data, SlotKind::Ref));
asm.emit(load(slots.index, SlotKind::Int));
asm.emit(load(slots.inner, SlotKind::Int));
asm.emit(int_op(Arith::Add));
asm.emit(Insn::Dup2);
asm.emit(Insn::ArrayLoad(ArrayKind::Byte));
asm.emit(load(slots.byte, SlotKind::Int));
asm.emit(int_op(Arith::Xor));
asm.emit(Insn::ArrayStore(ArrayKind::Byte));
asm.emit(Insn::Iinc {
index: slots.inner,
delta: 1,
});
asm.emit(Insn::Goto(inner));
asm.mark(inner_done);
asm.emit(load(slots.index, SlotKind::Int));
asm.emit(c_int(8));
asm.emit(int_op(Arith::Add));
asm.emit(store(slots.index, SlotKind::Int));
asm.emit(Insn::Goto(outer));
asm.mark(outer_done);
}
/// Один `next_u64` xoshiro256++ в `stream` (точное зеркало `Rng`).
fn emit_next(asm: &mut Gen, slots: &Slots) {
let s = slots.state;
// result = rotl(s0 + s3, 23) + s0
asm.emit(load(s[0], SlotKind::Long));
asm.emit(load(s[3], SlotKind::Long));
asm.emit(long_op(Arith::Add));
asm.emit(store(slots.tmp_a, SlotKind::Long));
asm.emit(load(slots.tmp_a, SlotKind::Long));
asm.emit(c_int(23));
asm.emit(shift(Arith::Shl));
asm.emit(store(slots.tmp_b, SlotKind::Long));
asm.emit(load(slots.tmp_a, SlotKind::Long));
asm.emit(c_int(41));
asm.emit(shift(Arith::Ushr));
asm.emit(load(slots.tmp_b, SlotKind::Long));
asm.emit(long_op(Arith::Or));
asm.emit(load(s[0], SlotKind::Long));
asm.emit(long_op(Arith::Add));
asm.emit(store(slots.stream, SlotKind::Long));
// t = s1 << 17
asm.emit(load(s[1], SlotKind::Long));
asm.emit(c_int(17));
asm.emit(shift(Arith::Shl));
asm.emit(store(slots.tmp_c, SlotKind::Long));
// s2 ^= s0; s3 ^= s1; s1 ^= s2; s0 ^= s3 — строго по порядку.
xor_into(asm, s[2], s[0]);
xor_into(asm, s[3], s[1]);
xor_into(asm, s[1], s[2]);
xor_into(asm, s[0], s[3]);
// s2 ^= t
xor_into(asm, s[2], slots.tmp_c);
// s3 = rotl(s3, 45)
asm.emit(load(s[3], SlotKind::Long));
asm.emit(store(slots.tmp_a, SlotKind::Long));
asm.emit(load(slots.tmp_a, SlotKind::Long));
asm.emit(c_int(45));
asm.emit(shift(Arith::Shl));
asm.emit(store(slots.tmp_b, SlotKind::Long));
asm.emit(load(slots.tmp_a, SlotKind::Long));
asm.emit(c_int(19));
asm.emit(shift(Arith::Ushr));
asm.emit(load(slots.tmp_b, SlotKind::Long));
asm.emit(long_op(Arith::Or));
asm.emit(store(s[3], SlotKind::Long));
}
/// `dst ^= src` для long-слотов.
fn xor_into(asm: &mut Gen, dst: u16, src: u16) {
asm.emit(load(dst, SlotKind::Long));
asm.emit(load(src, SlotKind::Long));
asm.emit(long_op(Arith::Xor));
asm.emit(store(dst, SlotKind::Long));
}
/// `ladd/lsub/...` для `long` (операнды уже на стеке).
fn long_op(op: Arith) -> Insn {
Insn::Binary {
prim: Prim::Long,
op,
}
}
/// Сдвиг `long` на `int` (`lshl`/`lushr`, операнды уже на стеке).
fn shift(op: Arith) -> Insn {
Insn::Binary {
prim: Prim::Long,
op,
}
}
/// `iadd/isub/...` для `int` (операнды уже на стеке).
fn int_op(op: Arith) -> Insn {
Insn::Binary {
prim: Prim::Int,
op,
}
}

View file

@ -0,0 +1,245 @@
//! Ветвь `xtea-ctr`: XTEA в режиме счётчика, побайтовый XOR на месте.
//!
//! Зеркало `scheme::xtea_ctr` побитово: ключ — четыре `u32` из key/nonce,
//! счётчик — номер чанка, все правые сдвиги — логические (`u32` в Rust).
use noxis_classfile::{Arith, ArrayKind, BranchCond, Cond, Convert, Insn, Prim, SlotKind};
use super::kdf::{Gen, c_int, load, store};
/// XTEA delta — знаковое представление для `ldc_w`.
const DELTA: i32 = 0x9E37_79B9u32 as i32;
/// Расшифровать `data` на месте; `len` — длина массива.
pub(super) fn emit_xtea(asm: &mut Gen, key: u16, nonce: u16, data: u16, len: u16) {
let slots = Slots::alloc(asm);
emit_key_array(asm, key, nonce, &slots);
asm.emit(c_int(0));
asm.emit(store(slots.chunk, SlotKind::Int));
let outer = asm.label();
let done = asm.label();
asm.mark(outer);
asm.emit(load(slots.chunk, SlotKind::Int));
asm.emit(load(len, SlotKind::Int));
asm.emit(Insn::Branch(BranchCond::Int(Cond::Ge), done));
emit_counter(asm, &slots);
emit_rounds(asm, &slots);
emit_xor_bytes(asm, data, len, &slots);
asm.emit(load(slots.chunk, SlotKind::Int));
asm.emit(c_int(8));
asm.emit(int_op(Arith::Add));
asm.emit(store(slots.chunk, SlotKind::Int));
asm.emit(Insn::Goto(outer));
asm.mark(done);
}
/// Слоты ветви.
struct Slots {
keys: u16,
chunk: u16,
value0: u16,
value1: u16,
sum: u16,
round: u16,
counter: u16,
tmp_a: u16,
tmp_b: u16,
src: u16,
shift: u16,
sub: u16,
index: u16,
}
impl Slots {
fn alloc(asm: &mut Gen) -> Self {
Self {
keys: asm.alloc(false),
chunk: asm.alloc(false),
value0: asm.alloc(false),
value1: asm.alloc(false),
sum: asm.alloc(false),
round: asm.alloc(false),
counter: asm.alloc(false),
tmp_a: asm.alloc(false),
tmp_b: asm.alloc(false),
src: asm.alloc(false),
shift: asm.alloc(false),
sub: asm.alloc(false),
index: asm.alloc(false),
}
}
}
/// `k = [key.lo, key.hi, nonce.lo, nonce.hi]` как `int[4]`.
fn emit_key_array(asm: &mut Gen, key: u16, nonce: u16, slots: &Slots) {
asm.emit(c_int(4));
asm.emit(Insn::NewArray(ArrayKind::Int));
asm.emit(store(slots.keys, SlotKind::Ref));
for (position, (source, high)) in [(key, false), (key, true), (nonce, false), (nonce, true)]
.into_iter()
.enumerate()
{
asm.emit(load(slots.keys, SlotKind::Ref));
asm.emit(c_int(position as i32));
asm.emit(load(source, SlotKind::Long));
if high {
asm.emit(c_int(32));
asm.emit(long_shift(Arith::Ushr));
asm.emit(Insn::Convert(Convert::L2I));
} else {
asm.emit(Insn::Convert(Convert::L2I));
}
asm.emit(Insn::ArrayStore(ArrayKind::Int));
}
}
/// Счётчик чанка: `v0 = 0`, `v1 = chunk`, `sum = round = 0`.
fn emit_counter(asm: &mut Gen, slots: &Slots) {
asm.emit(load(slots.chunk, SlotKind::Int));
asm.emit(c_int(3));
asm.emit(int_op(Arith::Ushr));
asm.emit(store(slots.counter, SlotKind::Int));
asm.emit(c_int(0));
asm.emit(store(slots.value0, SlotKind::Int));
asm.emit(load(slots.counter, SlotKind::Int));
asm.emit(store(slots.value1, SlotKind::Int));
asm.emit(c_int(0));
asm.emit(store(slots.sum, SlotKind::Int));
asm.emit(c_int(0));
asm.emit(store(slots.round, SlotKind::Int));
}
/// 32 раунда XTEA над `(v0, v1, sum)`.
fn emit_rounds(asm: &mut Gen, slots: &Slots) {
let again = asm.label();
let done = asm.label();
asm.mark(again);
asm.emit(load(slots.round, SlotKind::Int));
asm.emit(c_int(32));
asm.emit(Insn::Branch(BranchCond::Int(Cond::Ge), done));
mixing(asm, slots.value0, slots.value1, 0, slots);
asm.emit(load(slots.sum, SlotKind::Int));
asm.emit(c_int(DELTA));
asm.emit(int_op(Arith::Add));
asm.emit(store(slots.sum, SlotKind::Int));
mixing(asm, slots.value1, slots.value0, 11, slots);
asm.emit(Insn::Iinc {
index: slots.round,
delta: 1,
});
asm.emit(Insn::Goto(again));
asm.mark(done);
}
/// Половина раунда: `dst += (((src<<4)^(src>>>5)) + src) ^ (sum + k[(sum>>>s)&3])`.
fn mixing(asm: &mut Gen, dst: u16, src: u16, key_shift: i32, slots: &Slots) {
asm.emit(load(src, SlotKind::Int));
asm.emit(c_int(4));
asm.emit(int_op(Arith::Shl));
asm.emit(store(slots.tmp_a, SlotKind::Int));
asm.emit(load(src, SlotKind::Int));
asm.emit(c_int(5));
asm.emit(int_op(Arith::Ushr));
asm.emit(store(slots.tmp_b, SlotKind::Int));
asm.emit(load(slots.tmp_a, SlotKind::Int));
asm.emit(load(slots.tmp_b, SlotKind::Int));
asm.emit(int_op(Arith::Xor));
asm.emit(load(src, SlotKind::Int));
asm.emit(int_op(Arith::Add));
asm.emit(store(slots.tmp_a, SlotKind::Int));
asm.emit(load(slots.sum, SlotKind::Int));
asm.emit(load(slots.keys, SlotKind::Ref));
asm.emit(load(slots.sum, SlotKind::Int));
// Первая половина: `sum & 3` без сдвига; вторая: `(sum >>> s) & 3`.
if key_shift != 0 {
asm.emit(c_int(key_shift));
asm.emit(int_op(Arith::Ushr));
}
asm.emit(c_int(3));
asm.emit(int_op(Arith::And));
asm.emit(Insn::ArrayLoad(ArrayKind::Int));
asm.emit(int_op(Arith::Add));
asm.emit(store(slots.tmp_b, SlotKind::Int));
asm.emit(load(slots.tmp_a, SlotKind::Int));
asm.emit(load(slots.tmp_b, SlotKind::Int));
asm.emit(int_op(Arith::Xor));
asm.emit(load(dst, SlotKind::Int));
asm.emit(int_op(Arith::Add));
asm.emit(store(dst, SlotKind::Int));
}
/// XOR 8 байт чанка со стримом `(v0, v1)`; хвост короче 8 — частично.
fn emit_xor_bytes(asm: &mut Gen, data: u16, len: u16, slots: &Slots) {
asm.emit(c_int(0));
asm.emit(store(slots.index, SlotKind::Int));
let again = asm.label();
let done = asm.label();
let use_v0 = asm.label();
let have_src = asm.label();
asm.mark(again);
asm.emit(load(slots.index, SlotKind::Int));
asm.emit(c_int(8));
asm.emit(Insn::Branch(BranchCond::Int(Cond::Ge), done));
asm.emit(load(len, SlotKind::Int));
asm.emit(load(slots.chunk, SlotKind::Int));
asm.emit(load(slots.index, SlotKind::Int));
asm.emit(int_op(Arith::Add));
asm.emit(Insn::Branch(BranchCond::Int(Cond::Le), done));
asm.emit(load(slots.index, SlotKind::Int));
asm.emit(c_int(4));
asm.emit(int_op(Arith::And));
asm.emit(Insn::Branch(BranchCond::Zero(Cond::Eq), use_v0));
asm.emit(load(slots.value1, SlotKind::Int));
asm.emit(Insn::Goto(have_src));
asm.mark(use_v0);
asm.emit(load(slots.value0, SlotKind::Int));
asm.mark(have_src);
asm.emit(store(slots.src, SlotKind::Int));
asm.emit(load(slots.index, SlotKind::Int));
asm.emit(c_int(3));
asm.emit(int_op(Arith::And));
asm.emit(store(slots.tmp_a, SlotKind::Int));
asm.emit(c_int(3));
asm.emit(load(slots.tmp_a, SlotKind::Int));
asm.emit(int_op(Arith::Sub));
asm.emit(c_int(3));
asm.emit(int_op(Arith::Shl));
asm.emit(store(slots.shift, SlotKind::Int));
asm.emit(load(slots.src, SlotKind::Int));
asm.emit(load(slots.shift, SlotKind::Int));
asm.emit(int_op(Arith::Ushr));
asm.emit(c_int(0xFF));
asm.emit(int_op(Arith::And));
asm.emit(store(slots.sub, SlotKind::Int));
asm.emit(load(data, SlotKind::Ref));
asm.emit(load(slots.chunk, SlotKind::Int));
asm.emit(load(slots.index, SlotKind::Int));
asm.emit(int_op(Arith::Add));
asm.emit(Insn::Dup2);
asm.emit(Insn::ArrayLoad(ArrayKind::Byte));
asm.emit(load(slots.sub, SlotKind::Int));
asm.emit(int_op(Arith::Xor));
asm.emit(Insn::ArrayStore(ArrayKind::Byte));
asm.emit(Insn::Iinc {
index: slots.index,
delta: 1,
});
asm.emit(Insn::Goto(again));
asm.mark(done);
}
/// Сдвиг `long` на `int` (`lshl`/`lushr`).
fn long_shift(op: Arith) -> Insn {
Insn::Binary {
prim: Prim::Long,
op,
}
}
/// `iadd/isub/...` для `int`.
fn int_op(op: Arith) -> Insn {
Insn::Binary {
prim: Prim::Int,
op,
}
}

View file

@ -0,0 +1,10 @@
//! # noxis-runtime
//!
//! Схемы шифрования строк и ABI рантайм-декодера, который получает байткод.
pub mod codec;
pub mod decoder;
pub mod scheme;
pub use decoder::{DECODER_DESC, DECODER_METHOD, decoder_class, decoder_owner};
pub use scheme::{Blob, Scheme, decrypt, encrypt};

View file

@ -0,0 +1,203 @@
//! Схемы шифрования строк: пер-строчный ключ, nonce и несколько алгоритмов.
//!
//! Требования (см. `info/evo-weaknesses.md` и `docs/techniques.md`):
//! - **разные схемы между строками** — один скрипт не покрывает весь jar;
//! - **ключ не рядом с шифротекстом** — на вызов передаётся только salt,
//! мастер-секрет живёт в рантайм-декодере (сборка мастер-секрета — P2);
//! - **nonce уникален на строку** — keystream не повторяется;
//! - **MAC** — повреждение/подмена шифротекста ловится, а не молча портит текст.
use noxis_core::{Rng, derive_seed};
use crate::codec;
/// Ошибка схемы.
#[derive(Debug, thiserror::Error, PartialEq, Eq)]
pub enum SchemeError {
#[error("неизвестная схема `{0}`")]
UnknownScheme(String),
#[error("повреждённый шифротекст")]
CorruptCiphertext,
}
/// Поддерживаемые схемы.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Scheme {
/// XOR со keystream из xoshiro-like генератора (быстро, дёшево).
XorSplitmix,
/// XTEA в режиме счётчика (блочная, другая структура шифротекста).
XteaCtr,
}
impl Scheme {
/// Имя схемы, как она пишется в конфиге.
pub const fn name(self) -> &'static str {
match self {
Scheme::XorSplitmix => "xor-splitmix",
Scheme::XteaCtr => "xtea-ctr",
}
}
/// Разбор имени из конфига.
pub fn parse(name: &str) -> Result<Self, SchemeError> {
match name {
"xor-splitmix" => Ok(Scheme::XorSplitmix),
"xtea-ctr" => Ok(Scheme::XteaCtr),
other => Err(SchemeError::UnknownScheme(other.to_string())),
}
}
/// Все схемы в порядке объявления (для ротации).
pub const fn all() -> [Scheme; 2] {
[Scheme::XorSplitmix, Scheme::XteaCtr]
}
}
/// Зашифрованный блоб: шифротекст (base64) + tag для целостности.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Blob {
/// base64 шифротекста.
pub cipher: String,
/// 32-битный MAC по ключу от plaintext — ловит подмену/порчу.
pub tag: u32,
}
/// Ключ строки из мастер-секрета и salt (то же вычисление делает Java-декодер).
pub fn string_key(master: u64, salt: i32) -> u64 {
derive_seed(master, "string/key", u64::from(salt as u32))
}
/// Nonce строки: детерминированный от salt, но другой домен — keystream не
/// пересекается даже при совпадении ключей.
pub fn string_nonce(master: u64, salt: i32) -> u64 {
derive_seed(master, "string/nonce", u64::from(salt as u32))
}
/// Зашифровать строку.
pub fn encrypt(scheme: Scheme, master: u64, salt: i32, plain: &str) -> Blob {
let key = string_key(master, salt);
let nonce = string_nonce(master, salt);
let mut data = plain.as_bytes().to_vec();
match scheme {
Scheme::XorSplitmix => xor_splitmix(key, nonce, &mut data),
Scheme::XteaCtr => xtea_ctr(key, nonce, &mut data),
}
Blob {
cipher: codec::encode(&data),
tag: mac(key, &data),
}
}
/// Расшифровать блоб (зеркало того, что делает рантайм-декодер).
pub fn decrypt(scheme: Scheme, master: u64, salt: i32, blob: &Blob) -> Result<String, SchemeError> {
let key = string_key(master, salt);
let nonce = string_nonce(master, salt);
let mut data = codec::decode(&blob.cipher).map_err(|_| SchemeError::CorruptCiphertext)?;
if mac(key, &data) != blob.tag {
return Err(SchemeError::CorruptCiphertext);
}
match scheme {
Scheme::XorSplitmix => xor_splitmix(key, nonce, &mut data),
Scheme::XteaCtr => xtea_ctr(key, nonce, &mut data),
}
String::from_utf8(data).map_err(|_| SchemeError::CorruptCiphertext)
}
/// MAC: fnv1a поверх keystream-домена (дёшево, ловит точечные правки).
fn mac(key: u64, data: &[u8]) -> u32 {
let mut hash: u64 = 0xcbf2_9ce4_8422_2325;
for b in data.iter().chain(key.to_le_bytes().iter()) {
hash ^= u64::from(*b);
hash = hash.wrapping_mul(0x0000_0100_0000_01B3);
}
(hash ^ (hash >> 32)) as u32
}
fn xor_splitmix(key: u64, nonce: u64, data: &mut [u8]) {
let mut rng = Rng::from_seed(key ^ nonce.rotate_left(17));
for chunk in data.chunks_mut(8) {
let stream = rng.next_u64().to_le_bytes();
for (b, s) in chunk.iter_mut().zip(stream.iter()) {
*b ^= s;
}
}
}
/// XTEA в режиме счётчика: keystream = XTEA(k, counter), XOR — на месте.
fn xtea_ctr(key: u64, nonce: u64, data: &mut [u8]) {
let k = [
key as u32,
(key >> 32) as u32,
nonce as u32,
(nonce >> 32) as u32,
];
for (i, chunk) in data.chunks_mut(8).enumerate() {
let stream = xtea_encrypt_block(k, (i as u64).to_be_bytes());
for (b, s) in chunk.iter_mut().zip(stream.iter()) {
*b ^= s;
}
}
}
/// Один блок XTEA (32 раунда, delta = 0x9E3779B9).
fn xtea_encrypt_block(k: [u32; 4], block: [u8; 8]) -> [u8; 8] {
let mut v0 = u32::from_be_bytes([block[0], block[1], block[2], block[3]]);
let mut v1 = u32::from_be_bytes([block[4], block[5], block[6], block[7]]);
let mut sum: u32 = 0;
const DELTA: u32 = 0x9E37_79B9;
for _ in 0..32 {
v0 = v0.wrapping_add(
(((v1 << 4) ^ (v1 >> 5)).wrapping_add(v1)) ^ (sum.wrapping_add(k[(sum & 3) as usize])),
);
sum = sum.wrapping_add(DELTA);
v1 = v1.wrapping_add(
(((v0 << 4) ^ (v0 >> 5)).wrapping_add(v0))
^ (sum.wrapping_add(k[((sum >> 11) & 3) as usize])),
);
}
let mut out = [0u8; 8];
out[..4].copy_from_slice(&v0.to_be_bytes());
out[4..].copy_from_slice(&v1.to_be_bytes());
out
}
#[cfg(test)]
mod tests {
#![allow(clippy::expect_used, clippy::unwrap_used)]
use super::*;
#[test]
fn roundtrip_all_schemes() {
for scheme in Scheme::all() {
for text in ["", "a", "Hello, мир!", "x".repeat(33).as_str()] {
let blob = encrypt(scheme, 42, 7, text);
let back = decrypt(scheme, 42, 7, &blob).expect("decrypt");
assert_eq!(back, text, "scheme={:?}", scheme);
}
}
}
#[test]
fn different_salt_different_cipher() {
let a = encrypt(Scheme::XorSplitmix, 1, 1, "same");
let b = encrypt(Scheme::XorSplitmix, 1, 2, "same");
assert_ne!(a.cipher, b.cipher);
}
#[test]
fn tampering_is_detected() {
let mut blob = encrypt(Scheme::XteaCtr, 5, 3, "sensitive");
let mut bytes = codec::decode(&blob.cipher).expect("b64");
bytes[0] ^= 0xFF;
blob.cipher = codec::encode(&bytes);
assert_eq!(
decrypt(Scheme::XteaCtr, 5, 3, &blob),
Err(SchemeError::CorruptCiphertext)
);
}
#[test]
fn unknown_scheme_rejected() {
assert!(Scheme::parse("rot13").is_err());
}
}

View file

@ -0,0 +1,121 @@
//! Сквозная проверка декодера: шифруем в Rust, расшифровываем настоящим `java`.
//!
//! Собираются два класса (декодер + вызывающий `main`), пишутся на диск,
//! `java` исполняет — stdout обязан совпасть с исходными строками. Без `java`
//! тест пропускается (graceful skip, как в `noxis-classfile/tests/frames.rs`).
#![allow(clippy::expect_used, clippy::unwrap_used)]
use std::path::PathBuf;
use noxis_classfile::{ClassBuilder, Code, Const, Insn, InvokeKind, format};
use noxis_runtime::{Scheme, decoder_class, encrypt};
const MASTER: u64 = 0x1234_5678_9ABC_DEF0;
const DECODER: &str = "test/Decoder";
const CALLER: &str = "test/Call";
/// Строки разного размера и алфавита, покрывающие обе схемы.
fn samples() -> Vec<(String, Scheme)> {
vec![
("hi".to_string(), Scheme::XorSplitmix),
("Hello, мир!".to_string(), Scheme::XorSplitmix),
("x".repeat(100), Scheme::XorSplitmix),
("hi".to_string(), Scheme::XteaCtr),
("Hello, мир!".to_string(), Scheme::XteaCtr),
("x".repeat(100), Scheme::XteaCtr),
]
}
/// Класс-вызыватель: `main` печатает расшифровки.
fn caller_class(parts: &[(String, i32)]) -> noxis_classfile::ClassFile {
let mut insns = Vec::new();
for (cipher, salt) in parts {
insns.push(Insn::GetStatic {
owner: "java/lang/System".to_string(),
name: "out".to_string(),
descriptor: "Ljava/io/PrintStream;".to_string(),
});
insns.push(Insn::Const(Const::String(cipher.clone())));
insns.push(Insn::Const(Const::Int(*salt)));
insns.push(Insn::Invoke {
kind: InvokeKind::Static,
owner: DECODER.to_string(),
name: "d".to_string(),
descriptor: "(Ljava/lang/String;I)Ljava/lang/String;".to_string(),
itf: false,
});
insns.push(Insn::Invoke {
kind: InvokeKind::Virtual,
owner: "java/io/PrintStream".to_string(),
name: "println".to_string(),
descriptor: "(Ljava/lang/String;)V".to_string(),
itf: false,
});
}
insns.push(Insn::Return(None));
let code = Code {
max_stack: 4,
max_locals: 1,
insns,
exception_table: Vec::new(),
frames: Vec::new(),
attributes: Vec::new(),
};
ClassBuilder::new(CALLER, None)
.method_with_code(0x0009, "main", "([Ljava/lang/String;)V", code)
.build()
}
#[test]
fn decoder_executes_under_java() {
if std::process::Command::new("java")
.arg("-version")
.output()
.is_err()
{
eprintln!("SKIP: нет java");
return;
}
let samples = samples();
let mut parts = Vec::new();
let mut expected = Vec::new();
for (index, (text, scheme)) in samples.iter().enumerate() {
let salt = (index as i32)
.saturating_mul(2)
.saturating_add(match scheme {
Scheme::XorSplitmix => 0,
Scheme::XteaCtr => 1,
});
let blob = encrypt(*scheme, MASTER, salt, text);
parts.push((blob.cipher, salt));
expected.push(text.to_string());
}
let decoder = decoder_class(DECODER, MASTER, &["xor-splitmix", "xtea-ctr"]).expect("decoder");
let caller = caller_class(&parts);
let dir: PathBuf = std::env::temp_dir().join(format!("noxis-exec-{}", std::process::id()));
for (name, class) in [(DECODER, &decoder), (CALLER, &caller)] {
let mut path = dir.clone();
for part in name.split('/') {
path.push(part);
}
path.set_extension("class");
std::fs::create_dir_all(path.parent().expect("parent")).expect("mkdir");
std::fs::write(&path, format::write(class).expect("write")).expect("write class");
}
let out = std::process::Command::new("java")
.arg("-Xverify:all")
.arg("-cp")
.arg(&dir)
.arg(CALLER.replace('/', "."))
.output()
.expect("запуск java");
let stdout = String::from_utf8_lossy(&out.stdout);
let stderr = String::from_utf8_lossy(&out.stderr);
let _ = std::fs::remove_dir_all(&dir);
assert!(out.status.success(), "java упал:\n{stderr}\n{stdout}");
let lines: Vec<&str> = stdout.lines().collect();
let want: Vec<&str> = expected.iter().map(String::as_str).collect();
assert_eq!(lines, want, "расшифровка не сошлась");
}

View file

@ -0,0 +1,13 @@
[package]
name = "noxis-structure-check"
description = "Автогейт структурных правил проекта при компиляции"
version.workspace = true
edition.workspace = true
rust-version.workspace = true
license.workspace = true
[lib]
path = "src/lib.rs"
[lints]
workspace = true

Some files were not shown because too many files have changed in this diff Show more