frames,jar,passes: verifier-driven hardening from real jars
Proven by paranoid runs over lorep (17 classes, fully green), LoVisual (4081 classes) and oc2r (1278 classes) with per-class java -Xverify:all, each fix first reproduced as a red test: - merge: Top absorbs instead of Top-wins (javac declares Top for slots unwritten on some inflow; precise types reject those edges). Regression: loop_head_keeps_top_for_loop_local. - emit: collapse [Long|Double, Top] pairs on operand stacks (javac writes one entry; the pair is rejected). Regression: wide_stack_join_emits_single_entries. - xfer ArrayLoad: push the wide pair, fixing lcmp underflow. Regression: array_load_wide_pushes_pair. - xfer <init>: convert only the receiver's own allocation site instead of every Uninitialized (nested construction clobbered the outer object). Regression: nested_construction_keeps_outer. - xfer ANewArray: array operands gain one dimension ([B] -> [[B) instead of an illegal L-wrap ([L[B;). Covered by the same test. - jar load: duplicate class names (multi-release module-info twins) keep the first occurrence instead of crashing save. Test: duplicate_class_names_keep_first. - strings/constants: skip methods past MAX_METHOD_INSNS (8000) so giants cannot overflow the 64KB writer ceiling. Tests: oversized_methods_are_skipped in both passes. - merge errors now name both types being merged. Also: em-dash removed repo-wide (724 spots, rule added to todo.md), allow-lints only in tests (scratch example deleted), code comments migrated to English in every file this change touches.
This commit is contained in:
parent
2172206e83
commit
139aea9a8f
96 changed files with 1053 additions and 831 deletions
|
|
@ -24,7 +24,7 @@ pub struct Entrypoints {
|
|||
}
|
||||
|
||||
impl Entrypoints {
|
||||
/// Собрать entrypoint'ы по программе (без keep-правил — их добавляет [`Self::merge`]).
|
||||
/// Собрать entrypoint'ы по программе (без keep-правил - их добавляет [`Self::merge`]).
|
||||
pub fn collect(program: &Program) -> Self {
|
||||
let mut out = Self::default();
|
||||
|
||||
|
|
@ -101,7 +101,7 @@ fn implements_serializable(program: &Program, start: &str) -> bool {
|
|||
return true;
|
||||
}
|
||||
let Some(class) = program.class(&name) else {
|
||||
// Внешний предок: считаем, что не знаем — консервативно не keep'им.
|
||||
// Внешний предок: считаем, что не знаем - консервативно не keep'им.
|
||||
continue;
|
||||
};
|
||||
if let Some(sup) = &class.super_class {
|
||||
|
|
@ -130,15 +130,15 @@ fn collect_service_interfaces(program: &Program) -> BTreeSet<String> {
|
|||
/// Линейный эвристический поиск имён в рефлексивных API.
|
||||
///
|
||||
/// Полноценный dataflow не нужен: если рядом с вызовом `Class.forName(...)` /
|
||||
/// `getMethod(...)` лежит строковый литерал — это имя видно статически,
|
||||
/// `getMethod(...)` лежит строковый литерал - это имя видно статически,
|
||||
/// значит, его нельзя переименовывать.
|
||||
fn collect_reflective_names(out: &mut Entrypoints, code: &noxis_classfile::Code) {
|
||||
/// API, чьи аргументы — **имена классов**.
|
||||
/// API, чьи аргументы - **имена классов**.
|
||||
const CLASS_API: &[(&str, &str)] = &[
|
||||
("java/lang/Class", "forName"),
|
||||
("java/lang/ClassLoader", "loadClass"),
|
||||
];
|
||||
/// API, чьи аргументы — **имена членов** (поле/метод).
|
||||
/// API, чьи аргументы - **имена членов** (поле/метод).
|
||||
const MEMBER_API: &[(&str, &str)] = &[
|
||||
("java/lang/Class", "getMethod"),
|
||||
("java/lang/Class", "getDeclaredMethod"),
|
||||
|
|
|
|||
|
|
@ -40,7 +40,7 @@ impl ClassHierarchy {
|
|||
.and_then(|n| n.super_class.clone())
|
||||
}
|
||||
|
||||
/// Есть ли класс в иерархии (в том числе из внешнего classpath — нет).
|
||||
/// Есть ли класс в иерархии (в том числе из внешнего classpath - нет).
|
||||
pub fn contains(&self, internal_name: &str) -> bool {
|
||||
self.nodes.contains_key(internal_name)
|
||||
}
|
||||
|
|
@ -57,7 +57,7 @@ impl ClassHierarchy {
|
|||
|
||||
/// Является ли `child` подклассом `parent` (в пределах программы).
|
||||
///
|
||||
/// Неполная иерархия (внешние зависимости не грузим) — это осознанно:
|
||||
/// Неполная иерархия (внешние зависимости не грузим) - это осознанно:
|
||||
/// обфускатор работает по артефакту, а не по всему classpath.
|
||||
pub fn is_subclass_of(&self, child: &str, parent: &str) -> bool {
|
||||
if child == parent {
|
||||
|
|
@ -137,10 +137,10 @@ pub enum RenameSafety {
|
|||
/// Оценка безопасности переименования метода.
|
||||
///
|
||||
/// Правила (сознательно консервативные, как в `OBF/InheritanceAnalyzer`):
|
||||
/// - `<init>`/`<clinit>` — никогда;
|
||||
/// - `private`/`static`/`final`/`<package-private>` внутри финального класса — можно;
|
||||
/// - `<init>`/`<clinit>` - никогда;
|
||||
/// - `private`/`static`/`final`/`<package-private>` внутри финального класса - можно;
|
||||
/// - `public`/`protected`/`package-private`, если класс не `final` и есть
|
||||
/// наследники (внутри артефакта или нет) — нельзя, пока нет доказательства.
|
||||
/// наследники (внутри артефакта или нет) - нельзя, пока нет доказательства.
|
||||
pub fn method_rename_safety(
|
||||
program: &Program,
|
||||
hierarchy: &ClassHierarchy,
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@ use regex::Regex;
|
|||
use crate::entrypoints::Entrypoints;
|
||||
use noxis_core::Config;
|
||||
|
||||
/// Скомпилированный glob: `*` — любые символы кроме `/`, `**` — всё, `?` — один.
|
||||
/// Скомпилированный glob: `*` - любые символы кроме `/`, `**` - всё, `?` - один.
|
||||
fn compile(pattern: &str) -> Result<Regex, regex::Error> {
|
||||
let mut out = String::from("^");
|
||||
let chars: Vec<char> = pattern.chars().collect();
|
||||
|
|
@ -107,7 +107,7 @@ impl KeepRules {
|
|||
/// Методы, на которые ссылаются `BootstrapMethods` (`invokedynamic`).
|
||||
///
|
||||
/// Переименование такого метода без правки bootstrap-записи ломает класс,
|
||||
/// а правка — задача P2 (см. `todo.md`): пока просто не трогаем имена.
|
||||
/// а правка - задача P2 (см. `todo.md`): пока просто не трогаем имена.
|
||||
pub fn absorb_bootstrap(&mut self, program: &Program) {
|
||||
for class in &program.classes {
|
||||
for method in &class.methods {
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@
|
|||
//!
|
||||
//! Из этих данных пасс переименования узнаёт, **что** трогать нельзя.
|
||||
//! Урок из `info/evo-weaknesses.md`: у EvoVis рефлексия по имени была
|
||||
//! открытым текстом, а entrypoint'ы — в `fabric.mod.json`; здесь они
|
||||
//! открытым текстом, а entrypoint'ы - в `fabric.mod.json`; здесь они
|
||||
//! обязаны попадать в keep-набор автоматически.
|
||||
|
||||
pub mod entrypoints;
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
//! Emitting compact frames at the points where the verifier expects them.
|
||||
//!
|
||||
//! A frame's kind is chosen by comparing with the previous frame
|
||||
//! (`Same`/`Append`/`Chop`/`SameLocals`, otherwise `Full`) — like `javac`.
|
||||
//! (`Same`/`Append`/`Chop`/`SameLocals`, otherwise `Full`) - like `javac`.
|
||||
//! Trailing `Top`s are trimmed both when comparing and when writing: an empty
|
||||
//! slot is not a type.
|
||||
|
||||
|
|
@ -12,7 +12,7 @@ use super::xfer::State;
|
|||
use super::{StackMapFrame, VerificationType};
|
||||
use crate::error::ClassfileError;
|
||||
|
||||
/// Frames at every point in `targets` (sorted — deltas in order).
|
||||
/// Frames at every point in `targets` (sorted - deltas in order).
|
||||
///
|
||||
/// Deltas are in instruction numbers (like the backend after reading); the writer
|
||||
/// recomputes them into bytes itself.
|
||||
|
|
@ -50,7 +50,7 @@ fn select(
|
|||
previous: &[VerificationType],
|
||||
stack: &[VerificationType],
|
||||
) -> StackMapFrame {
|
||||
// Comparison is at variable granularity (`long` — one entry, like `javac`);
|
||||
// Comparison is at variable granularity (`long` - one entry, like `javac`);
|
||||
// otherwise halves would produce `Append{[Top, Long]}`.
|
||||
let current = vars(locals);
|
||||
let base = vars(previous);
|
||||
|
|
@ -98,7 +98,7 @@ fn select(
|
|||
}
|
||||
}
|
||||
|
||||
/// Stack entries → values: `[Double, Top]` — one entry (like `javac`).
|
||||
/// Stack entries → values: `[Double, Top]` - one entry (like `javac`).
|
||||
///
|
||||
/// Unlike [`vars`], no trailing-`Top` trimming: every stack entry is live by
|
||||
/// construction, and dropping one would corrupt the depth.
|
||||
|
|
@ -122,7 +122,7 @@ fn stack_vars(stack: &[VerificationType]) -> Vec<VerificationType> {
|
|||
out
|
||||
}
|
||||
|
||||
/// Slots → variables: `[Long, Top]` — one entry; trailing `Top`s — gone.
|
||||
/// Slots → variables: `[Long, Top]` - one entry; trailing `Top`s - gone.
|
||||
///
|
||||
/// Single `Top`s inside (holes) are kept: in `Full` they are a legal type, and we
|
||||
/// don't allow them in `Append` (see `select`).
|
||||
|
|
|
|||
|
|
@ -31,7 +31,7 @@ pub(crate) fn split_array(descriptor: &str) -> Option<(usize, &str)> {
|
|||
Some((dims, descriptor[dims..].trim_end_matches(';')))
|
||||
}
|
||||
|
||||
/// Slots for a single field descriptor (`long`/`double` — pair with `Top`).
|
||||
/// Slots for a single field descriptor (`long`/`double` - pair with `Top`).
|
||||
pub(crate) fn field_slots(descriptor: &str) -> Result<Vec<VerificationType>, DescError> {
|
||||
let mut chars = descriptor.chars();
|
||||
match chars.next() {
|
||||
|
|
@ -57,10 +57,10 @@ pub(crate) fn field_slots(descriptor: &str) -> Result<Vec<VerificationType>, Des
|
|||
}
|
||||
}
|
||||
|
||||
/// Parameter and return slots (`None` — `void`).
|
||||
/// Parameter and return slots (`None` - `void`).
|
||||
type MethodSlots = (Vec<Vec<VerificationType>>, Option<Vec<VerificationType>>);
|
||||
|
||||
/// Method descriptor: parameter slots and return slots (`None` — `void`).
|
||||
/// Method descriptor: parameter slots and return slots (`None` - `void`).
|
||||
pub(crate) fn method_slots(descriptor: &str) -> Result<MethodSlots, DescError> {
|
||||
let (params, ret) = descriptor
|
||||
.split_once('(')
|
||||
|
|
|
|||
|
|
@ -1,12 +1,12 @@
|
|||
//! Type merging (LUB) and descriptor parsing.
|
||||
//!
|
||||
//! The lattice: `Top` is "empty" (the neighbour wins), `Null` is absorbed into a
|
||||
//! reference, equal types map to themselves, references take the least upper
|
||||
//! bound over the hierarchy. Incompatible initialized types (`int` vs a reference
|
||||
//! after `astore` in a handler) yield `Top`, like `javac`'s truncated frames: a
|
||||
//! `Top` in a frame accepts any actual value, so this is always sound. The
|
||||
//! exception is uninitialized types: merging them means a broken construction
|
||||
//! flow — here an honest error, not a silent `Top`.
|
||||
//! The lattice: `Top` absorbs (a slot unwritten on any inflow stays `Top`,
|
||||
//! like `javac` - a precise type would reject the inflow that never wrote
|
||||
//! it). `Null` is absorbed into a reference, equal types map to themselves,
|
||||
//! references take the least upper bound over the hierarchy. Incompatible
|
||||
//! initialized types (`int` vs a reference) yield `Top`. The exception is
|
||||
//! uninitialized types: merging them means a broken construction flow -
|
||||
//! here an honest error, not a silent `Top`.
|
||||
|
||||
pub(super) mod descs;
|
||||
|
||||
|
|
@ -19,7 +19,7 @@ use descs::write_local;
|
|||
|
||||
/// The class hierarchy for LUB: the superclass chain is enough.
|
||||
pub trait Hierarchy {
|
||||
/// The direct superclass (`None` — `java/lang/Object` or unknown).
|
||||
/// The direct superclass (`None` - `java/lang/Object` or unknown).
|
||||
fn superclass(&self, class: &str) -> Option<String>;
|
||||
}
|
||||
|
||||
|
|
@ -38,7 +38,7 @@ impl Merger<'_> {
|
|||
}
|
||||
}
|
||||
|
||||
/// Merge an incoming state; `true` — the slot changed.
|
||||
/// Merge an incoming state; `true` - the slot changed.
|
||||
pub fn merge_into(
|
||||
&self,
|
||||
slot: &mut Option<State>,
|
||||
|
|
@ -75,11 +75,11 @@ impl Merger<'_> {
|
|||
VerificationType::Object(self.class.clone())
|
||||
};
|
||||
slot = write_local(&mut locals, slot, vec![this])
|
||||
.map_err(|_| self.invalid("параметры не влезают в locals"))?;
|
||||
.map_err(|_| self.invalid("parameters do not fit into locals"))?;
|
||||
}
|
||||
for param in params {
|
||||
slot = write_local(&mut locals, slot, param)
|
||||
.map_err(|_| self.invalid("параметры не влезают в locals"))?;
|
||||
.map_err(|_| self.invalid("parameters do not fit into locals"))?;
|
||||
}
|
||||
Ok(State {
|
||||
locals,
|
||||
|
|
@ -89,10 +89,10 @@ impl Merger<'_> {
|
|||
|
||||
fn lub_states(&self, first: &State, second: &State) -> Result<State, ClassfileError> {
|
||||
if first.locals.len() != second.locals.len() {
|
||||
return Err(self.invalid("разная длина locals при слиянии"));
|
||||
return Err(self.invalid("locals length differs across the merge"));
|
||||
}
|
||||
if first.stack.len() != second.stack.len() {
|
||||
return Err(self.invalid("разная глубина стека при слиянии"));
|
||||
return Err(self.invalid("stack depth differs across the merge"));
|
||||
}
|
||||
Ok(State {
|
||||
locals: first
|
||||
|
|
@ -120,7 +120,10 @@ impl Merger<'_> {
|
|||
return Ok(first.clone());
|
||||
}
|
||||
match (first, second) {
|
||||
(Top, other) | (other, Top) => Ok(other.clone()),
|
||||
// A slot unwritten on any inflow stays `Top` (like `javac`): the
|
||||
// frame keeps accepting every inflow, while a precise type would
|
||||
// reject the one that never wrote the slot.
|
||||
(Top, _) | (_, Top) => Ok(Top),
|
||||
(Null, other @ Object(_)) | (other @ Object(_), Null) => Ok(other.clone()),
|
||||
(Null, Null) => Ok(Null),
|
||||
(Integer, Integer) | (Float, Float) | (Long, Long) | (Double, Double) => {
|
||||
|
|
@ -128,10 +131,12 @@ impl Merger<'_> {
|
|||
}
|
||||
(Object(a), Object(b)) => Ok(Object(self.common_superclass(a, b))),
|
||||
// Equal ones are handled above; here uninitialized are always
|
||||
// different — that's a broken construction flow.
|
||||
// different - that's a broken construction flow.
|
||||
(VerificationType::UninitializedThis | VerificationType::Uninitialized(_), _)
|
||||
| (_, VerificationType::UninitializedThis | VerificationType::Uninitialized(_)) => {
|
||||
Err(self.invalid("слияние неинициализированных типов"))
|
||||
Err(self.invalid(&format!(
|
||||
"merging uninitialized types ({first:?} with {second:?})"
|
||||
)))
|
||||
}
|
||||
// Incompatible initialized (e.g. `int` from the `try` body and
|
||||
// `Throwable` from a handler in one slot): `Top`, like `javac`.
|
||||
|
|
@ -139,7 +144,7 @@ impl Merger<'_> {
|
|||
}
|
||||
}
|
||||
|
||||
/// Least common ancestor: climb the chains, fallback — `Object`.
|
||||
/// Least common ancestor: climb the chains, fallback - `Object`.
|
||||
fn common_superclass(&self, first: &str, second: &str) -> String {
|
||||
if first == second {
|
||||
return first.to_string();
|
||||
|
|
|
|||
|
|
@ -2,11 +2,11 @@
|
|||
//!
|
||||
//! Frames are the most expensive part of any Rust obfuscator: the JVM verifier
|
||||
//! (class version ≥ 50) demands an exact `StackMapTable`, or the class won't load.
|
||||
//! Implementation options and their costs — `docs/stack.md`.
|
||||
//! Implementation options and their costs - `docs/stack.md`.
|
||||
//!
|
||||
//! Method: CFG over labels → worklist with types (`xfer`) → LUB merge (`merge`)
|
||||
//! → emit compact frames (`emit`). Byte offsets come from the same encoding the
|
||||
//! writer uses — otherwise frames would drift apart from the bytes.
|
||||
//! writer uses - otherwise frames would drift apart from the bytes.
|
||||
|
||||
mod emit;
|
||||
mod flow;
|
||||
|
|
@ -113,7 +113,7 @@ pub fn compute_frames(
|
|||
emit::emit_frames(&states, &targets, &initial.locals, &merger)
|
||||
}
|
||||
|
||||
/// The byte offset of each instruction — using the same encoding as the writer.
|
||||
/// The byte offset of each instruction - using the same encoding as the writer.
|
||||
fn layout(
|
||||
bare: &[&Insn],
|
||||
labels: &BTreeMap<Label, u32>,
|
||||
|
|
@ -188,7 +188,7 @@ fn run_worklist(
|
|||
Ok(())
|
||||
}
|
||||
|
||||
/// Merge a state into an instruction; `true` — changed, keep walking.
|
||||
/// Merge a state into an instruction; `true` - changed, keep walking.
|
||||
fn flow_into(
|
||||
merger: &Merger<'_>,
|
||||
states: &mut [Option<State>],
|
||||
|
|
@ -220,7 +220,7 @@ fn flow_into(
|
|||
}
|
||||
|
||||
/// CFG edges without exceptions: fallthrough + branch targets.
|
||||
/// The exception type at a handler's entry (`catch-all` — any throwable).
|
||||
/// The exception type at a handler's entry (`catch-all` - any throwable).
|
||||
fn catch_type(catch: &Option<String>) -> VerificationType {
|
||||
match catch {
|
||||
Some(name) => VerificationType::Object(name.clone()),
|
||||
|
|
|
|||
191
crates/noxis-classfile/src/format/frames/tests/joins.rs
Normal file
191
crates/noxis-classfile/src/format/frames/tests/joins.rs
Normal file
|
|
@ -0,0 +1,191 @@
|
|||
//! Join-shape tests: loop heads, wide array loads, nested construction.
|
||||
|
||||
#![allow(clippy::expect_used, clippy::unwrap_used)]
|
||||
|
||||
use super::super::{StackMapFrame, VerificationType, compute_frames};
|
||||
use super::{NoHierarchy, code_method, load, load_ref, store, store_ref};
|
||||
use crate::insn::{BranchCond, Cond, Const, Insn, Prim};
|
||||
|
||||
/// `anewarray [B` (a partially bound multi-dim array) yields its descriptor
|
||||
/// verbatim: wrapping it again (`[L[B;`) is not a class and HotSpot rejects
|
||||
/// the pool entry.
|
||||
#[test]
|
||||
fn anewarray_of_array_keeps_descriptor() {
|
||||
let method = code_method(
|
||||
"make",
|
||||
"()Ljava/lang/Object;",
|
||||
2,
|
||||
1,
|
||||
vec![
|
||||
Insn::Mark(0),
|
||||
Insn::Const(Const::Int(3)),
|
||||
Insn::ANewArray("[B".to_string()),
|
||||
store_ref(0),
|
||||
load_ref(0),
|
||||
Insn::Branch(BranchCond::Null { is_null: false }, 1),
|
||||
Insn::AconstNull,
|
||||
store_ref(0),
|
||||
Insn::Goto(2),
|
||||
Insn::Mark(1),
|
||||
Insn::Mark(2),
|
||||
load_ref(0),
|
||||
Insn::AReturn,
|
||||
],
|
||||
vec![],
|
||||
);
|
||||
let frames = compute_frames("demo/Make", &method, &NoHierarchy).expect("frames");
|
||||
assert_eq!(
|
||||
frames,
|
||||
vec![StackMapFrame::Append {
|
||||
offset_delta: 8,
|
||||
locals: vec![VerificationType::Object("[[B".to_string())],
|
||||
},]
|
||||
);
|
||||
}
|
||||
|
||||
/// A loop head keeps `Top` for a slot written only inside the loop.
|
||||
///
|
||||
/// Inflows: entry (never wrote it) and back edge (wrote `Integer`). Like
|
||||
/// `javac`, the join declares `Top`: a precise type would reject the inflow
|
||||
/// that never wrote the slot (`Type top is not assignable`).
|
||||
#[test]
|
||||
fn loop_head_keeps_top_for_loop_local() {
|
||||
let method = code_method(
|
||||
"sum",
|
||||
"()I",
|
||||
2,
|
||||
3,
|
||||
vec![
|
||||
Insn::Mark(0),
|
||||
Insn::Const(Const::Int(0)),
|
||||
store(0),
|
||||
Insn::Const(Const::Int(0)),
|
||||
store(1),
|
||||
Insn::Mark(1),
|
||||
load(1),
|
||||
Insn::Const(Const::Int(10)),
|
||||
Insn::Branch(BranchCond::Int(Cond::Ge), 2),
|
||||
load(1),
|
||||
store(2),
|
||||
load(2),
|
||||
store(0),
|
||||
Insn::Iinc { index: 1, delta: 1 },
|
||||
Insn::Goto(1),
|
||||
Insn::Mark(2),
|
||||
load(0),
|
||||
Insn::Return(Some(Prim::Int)),
|
||||
],
|
||||
vec![],
|
||||
);
|
||||
let frames = compute_frames("demo/Sum", &method, &NoHierarchy).expect("frames");
|
||||
assert_eq!(
|
||||
frames,
|
||||
vec![
|
||||
StackMapFrame::Append {
|
||||
offset_delta: 4,
|
||||
locals: vec![VerificationType::Integer, VerificationType::Integer],
|
||||
},
|
||||
StackMapFrame::Same { offset_delta: 8 },
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
/// A nested construction (`new Outer(new Inner())`) initializes only the
|
||||
/// inner object: the outer `Uninitialized` survives with its own offset, so
|
||||
/// a later join still sees it instead of a clobbered `Object(Inner)`.
|
||||
#[test]
|
||||
fn nested_construction_keeps_outer_uninitialized() {
|
||||
let method = code_method(
|
||||
"make",
|
||||
"()I",
|
||||
4,
|
||||
1,
|
||||
vec![
|
||||
Insn::Mark(0),
|
||||
Insn::New("demo/Outer".to_string()),
|
||||
Insn::Dup,
|
||||
Insn::New("demo/Inner".to_string()),
|
||||
Insn::Dup,
|
||||
Insn::Invoke {
|
||||
kind: crate::insn::InvokeKind::Special,
|
||||
owner: "demo/Inner".to_string(),
|
||||
name: "<init>".to_string(),
|
||||
descriptor: "()V".to_string(),
|
||||
itf: false,
|
||||
},
|
||||
Insn::Invoke {
|
||||
kind: crate::insn::InvokeKind::Special,
|
||||
owner: "demo/Outer".to_string(),
|
||||
name: "<init>".to_string(),
|
||||
descriptor: "(Ldemo/Inner;)V".to_string(),
|
||||
itf: false,
|
||||
},
|
||||
store_ref(0),
|
||||
load_ref(0),
|
||||
Insn::Branch(BranchCond::Null { is_null: false }, 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![],
|
||||
);
|
||||
let frames = compute_frames("demo/Make", &method, &NoHierarchy).expect("frames");
|
||||
assert_eq!(
|
||||
frames,
|
||||
vec![
|
||||
StackMapFrame::Append {
|
||||
offset_delta: 11,
|
||||
locals: vec![VerificationType::Object("demo/Outer".to_string())],
|
||||
},
|
||||
StackMapFrame::SameLocals {
|
||||
offset_delta: 0,
|
||||
local: VerificationType::Integer,
|
||||
},
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
/// `laload` pushes a pair (`long` + `Top`): a following `lcmp` needs four
|
||||
/// entries and must not underflow.
|
||||
#[test]
|
||||
fn array_load_wide_pushes_pair() {
|
||||
let method = code_method(
|
||||
"get",
|
||||
"([JI)J",
|
||||
4,
|
||||
2,
|
||||
vec![
|
||||
Insn::Mark(0),
|
||||
load_ref(0),
|
||||
load(1),
|
||||
Insn::ArrayLoad(crate::insn::ArrayKind::Long),
|
||||
Insn::Const(Const::Long(0)),
|
||||
Insn::Cmp {
|
||||
prim: Prim::Long,
|
||||
nan_great: false,
|
||||
},
|
||||
Insn::Branch(BranchCond::Zero(Cond::Ne), 1),
|
||||
Insn::Const(Const::Long(1)),
|
||||
Insn::Goto(2),
|
||||
Insn::Mark(1),
|
||||
Insn::Const(Const::Long(0)),
|
||||
Insn::Mark(2),
|
||||
Insn::Return(Some(Prim::Long)),
|
||||
],
|
||||
vec![],
|
||||
);
|
||||
let frames = compute_frames("demo/Wide", &method, &NoHierarchy).expect("frames");
|
||||
assert_eq!(
|
||||
frames,
|
||||
vec![
|
||||
StackMapFrame::Same { offset_delta: 8 },
|
||||
StackMapFrame::SameLocals {
|
||||
offset_delta: 0,
|
||||
local: VerificationType::Long,
|
||||
},
|
||||
]
|
||||
);
|
||||
}
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
//! Unit-тесты `compute_frames` на синтезированном IR.
|
||||
//! Unit tests for `compute_frames` on synthesized IR.
|
||||
|
||||
#![allow(clippy::expect_used, clippy::unwrap_used)]
|
||||
|
||||
|
|
@ -7,8 +7,8 @@ use crate::Hierarchy;
|
|||
use crate::insn::{BranchCond, Cond, Const, Insn, Prim, SlotKind};
|
||||
use crate::model::{Code, ExceptionHandler, Method};
|
||||
|
||||
/// Пустая иерархия: всё неизвестное — наследники `Object` (для LUB хватает).
|
||||
struct NoHierarchy;
|
||||
/// Empty hierarchy: everything unknown descends from `Object` (enough for LUB).
|
||||
pub(super) struct NoHierarchy;
|
||||
|
||||
impl Hierarchy for NoHierarchy {
|
||||
fn superclass(&self, _class: &str) -> Option<String> {
|
||||
|
|
@ -16,7 +16,7 @@ impl Hierarchy for NoHierarchy {
|
|||
}
|
||||
}
|
||||
|
||||
fn code_method(
|
||||
pub(super) fn code_method(
|
||||
name: &str,
|
||||
descriptor: &str,
|
||||
max_stack: u16,
|
||||
|
|
@ -40,7 +40,7 @@ fn code_method(
|
|||
}
|
||||
}
|
||||
|
||||
/// Ветвь → два кадра (`Same`, `SameLocals`).
|
||||
/// A branch emits two frames (`Same`, `SameLocals`).
|
||||
#[test]
|
||||
fn branch_emits_same_frames() {
|
||||
let mut method = code_method(
|
||||
|
|
@ -76,7 +76,11 @@ fn branch_emits_same_frames() {
|
|||
);
|
||||
}
|
||||
|
||||
/// `try/catch`: handler получает `Full`, продолжение — `Append`.
|
||||
/// `try/catch`: the handler gets `Full`, the continuation - `Same`.
|
||||
///
|
||||
/// Slot 1 is written only inside the handler, so the join keeps `Top` there
|
||||
/// (like `javac`): a precise type would reject the inflow that never wrote
|
||||
/// it (`Type top is not assignable`).
|
||||
#[test]
|
||||
fn handler_emits_full_frame() {
|
||||
let method = code_method(
|
||||
|
|
@ -115,15 +119,12 @@ fn handler_emits_full_frame() {
|
|||
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())],
|
||||
},
|
||||
StackMapFrame::Same { offset_delta: 2 },
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
/// Два `new` на разных ветвях нельзя слить — честная ошибка, не `Top`.
|
||||
/// Two `new` on different branches cannot merge - an honest error, not `Top`.
|
||||
#[test]
|
||||
fn uninitialized_merge_is_rejected() {
|
||||
let method = code_method(
|
||||
|
|
@ -150,28 +151,37 @@ fn uninitialized_merge_is_rejected() {
|
|||
],
|
||||
vec![],
|
||||
);
|
||||
let err = compute_frames("demo/Pick", &method, &NoHierarchy).expect_err("слияние");
|
||||
let err = compute_frames("demo/Pick", &method, &NoHierarchy).expect_err("merge");
|
||||
assert!(
|
||||
err.to_string().contains("неинициализированных"),
|
||||
"не тот текст: {err}"
|
||||
err.to_string().contains("uninitialized"),
|
||||
"wrong message: {err}"
|
||||
);
|
||||
}
|
||||
|
||||
fn load(slot: u16) -> Insn {
|
||||
mod joins;
|
||||
|
||||
pub(super) fn load(slot: u16) -> Insn {
|
||||
Insn::Load {
|
||||
slot,
|
||||
kind: SlotKind::Int,
|
||||
}
|
||||
}
|
||||
|
||||
fn store(slot: u16) -> Insn {
|
||||
pub(super) fn load_ref(slot: u16) -> Insn {
|
||||
Insn::Load {
|
||||
slot,
|
||||
kind: SlotKind::Ref,
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) fn store(slot: u16) -> Insn {
|
||||
Insn::Store {
|
||||
slot,
|
||||
kind: SlotKind::Int,
|
||||
}
|
||||
}
|
||||
|
||||
fn store_ref(slot: u16) -> Insn {
|
||||
pub(super) fn store_ref(slot: u16) -> Insn {
|
||||
Insn::Store {
|
||||
slot,
|
||||
kind: SlotKind::Ref,
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
//! Вызовы и константы: дескрипторы, `<init>`, слоты значений.
|
||||
//! Calls and constants: descriptors, `<init>`, value slots.
|
||||
|
||||
use super::super::VerificationType;
|
||||
use super::super::merge::descs::{DescError, field_slots, method_slots};
|
||||
|
|
@ -34,18 +34,12 @@ pub(super) fn invoke(
|
|||
descriptor: &str,
|
||||
) -> Result<(), ClassfileError> {
|
||||
let (params, ret) = method_slots(descriptor).map_err(|e| context(class, method, e))?;
|
||||
super::stack::pop_slots(
|
||||
state,
|
||||
count_slots(¶ms),
|
||||
class,
|
||||
method,
|
||||
"аргументы вызова",
|
||||
)?;
|
||||
super::stack::pop_slots(state, count_slots(¶ms), class, method, "call arguments")?;
|
||||
let mut receiver = None;
|
||||
if kind != InvokeKind::Static {
|
||||
receiver = super::stack::pop_slots(state, 1, class, method, "objectref")?.pop();
|
||||
}
|
||||
// `<init>` инициализирует получателя во ВСЕХ его копиях.
|
||||
// `<init>` initializes the receiver in ALL its copies.
|
||||
if name == "<init>" {
|
||||
match receiver {
|
||||
Some(VerificationType::UninitializedThis) => {
|
||||
|
|
@ -55,9 +49,11 @@ pub(super) fn invoke(
|
|||
&initialized(class),
|
||||
);
|
||||
}
|
||||
Some(VerificationType::Uninitialized(_)) => {
|
||||
// Only the receiver's own allocation site: an enclosing `new`
|
||||
// whose `<init>` has not run yet must stay uninitialized.
|
||||
Some(VerificationType::Uninitialized(offset)) => {
|
||||
let fresh = VerificationType::Object(owner.to_string());
|
||||
replace_uninit(state, &fresh);
|
||||
replace(state, &VerificationType::Uninitialized(offset), &fresh);
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
|
|
@ -68,12 +64,12 @@ pub(super) fn invoke(
|
|||
Ok(())
|
||||
}
|
||||
|
||||
/// Число слотов всех параметров.
|
||||
/// Slot count across all parameters.
|
||||
pub(super) fn count_slots(params: &[Vec<VerificationType>]) -> usize {
|
||||
params.iter().map(Vec::len).sum()
|
||||
}
|
||||
|
||||
/// Заменить все вхождения типа новым (инициализация через `<init>`).
|
||||
/// Replace every occurrence of a type with a new one (init via `<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 {
|
||||
|
|
@ -82,15 +78,6 @@ fn replace(state: &mut State, from: &VerificationType, to: &VerificationType) {
|
|||
}
|
||||
}
|
||||
|
||||
/// Любой `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())
|
||||
}
|
||||
|
|
@ -115,7 +102,7 @@ pub(super) fn prim_push(prim: Prim) -> Vec<VerificationType> {
|
|||
}
|
||||
}
|
||||
|
||||
/// `(слоты аргумента, примитив результата)` для конверсии.
|
||||
/// `(argument slots, result primitive)` for a conversion.
|
||||
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};
|
||||
|
|
|
|||
|
|
@ -1,8 +1,8 @@
|
|||
//! Состояние типов и transfer-функции инструкций.
|
||||
//! Type states and per-instruction transfer functions.
|
||||
//!
|
||||
//! Стек растёт вправо (`push` — в конец). `long`/`double` занимают два слота:
|
||||
//! значение + `Top`. Проверяется только ФОРМА (глубина, категории), не
|
||||
//! соответствие типов — неверные типы честно упадут на верификаторе JVM.
|
||||
//! The stack grows right (`push` appends). `long`/`double` take two slots:
|
||||
//! value + `Top`. Only the SHAPE is checked (depth, categories), not
|
||||
//! type agreement - wrong types honestly fail on the JVM verifier.
|
||||
|
||||
pub(super) mod calls;
|
||||
pub(super) mod stack;
|
||||
|
|
@ -15,14 +15,14 @@ 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 в одной точке программы.
|
||||
/// Local types plus the operand stack at one program point.
|
||||
#[derive(Debug, Clone, Default, PartialEq)]
|
||||
pub(super) struct State {
|
||||
pub locals: Vec<VerificationType>,
|
||||
pub stack: Vec<VerificationType>,
|
||||
}
|
||||
|
||||
/// Применить инструкцию к состоянию (на месте).
|
||||
/// Apply one instruction to the state (in place).
|
||||
pub(super) fn transfer(
|
||||
class: &str,
|
||||
method: &str,
|
||||
|
|
@ -31,7 +31,11 @@ pub(super) fn transfer(
|
|||
state: &mut State,
|
||||
) -> Result<(), ClassfileError> {
|
||||
match insn {
|
||||
Insn::Mark(label) => Err(invalid(class, method, &format!("маркер {label} не снят"))),
|
||||
Insn::Mark(label) => Err(invalid(
|
||||
class,
|
||||
method,
|
||||
&format!("marker {label} was not stripped"),
|
||||
)),
|
||||
Insn::Nop => Ok(()),
|
||||
Insn::AconstNull => push(state, vec![VerificationType::Null]),
|
||||
Insn::Const(value) => {
|
||||
|
|
@ -51,29 +55,29 @@ pub(super) fn transfer(
|
|||
Insn::Iinc { index, .. } => {
|
||||
let current = local_get(state, *index, class, method)?;
|
||||
if current != VerificationType::Integer {
|
||||
return Err(invalid(class, method, "iinc не по int"));
|
||||
return Err(invalid(class, method, "iinc over a non-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)])
|
||||
pop_slots(state, 1, class, method, "array index")?;
|
||||
let array = pop_slots(state, 1, class, method, "array")?.remove(0);
|
||||
push(state, wide_push(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, "массив")?;
|
||||
pop_slots(state, count, class, method, "value")?;
|
||||
pop_slots(state, 1, class, method, "array index")?;
|
||||
pop_slots(state, 1, class, method, "array")?;
|
||||
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"));
|
||||
return Err(invalid(class, method, "pop of a long/double half"));
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
|
@ -90,7 +94,7 @@ pub(super) fn transfer(
|
|||
Insn::Swap => {
|
||||
let mut taken = pop_slots(state, 2, class, method, "swap")?;
|
||||
if taken.contains(&VerificationType::Top) {
|
||||
return Err(invalid(class, method, "swap с половиной"));
|
||||
return Err(invalid(class, method, "swap with a half"));
|
||||
}
|
||||
taken.reverse();
|
||||
push(state, taken)
|
||||
|
|
@ -100,7 +104,7 @@ pub(super) fn transfer(
|
|||
if *op == Arith::Neg {
|
||||
pop_slots(state, slots as usize, class, method, "neg")?;
|
||||
} else if matches!(op, Arith::Shl | Arith::Shr | Arith::Ushr) {
|
||||
// Сдвиг берёт (значение, int), а не два значения.
|
||||
// A shift takes (value, int), not two values.
|
||||
pop_slots(state, slots as usize + 1, class, method, "shift")?;
|
||||
} else {
|
||||
pop_slots(state, 2 * slots as usize, class, method, "arith")?;
|
||||
|
|
@ -128,7 +132,7 @@ pub(super) fn transfer(
|
|||
Insn::Jsr(_) | Insn::Ret(_) => Err(invalid(
|
||||
class,
|
||||
method,
|
||||
"подпрограммы jsr/ret не поддерживаются",
|
||||
"jsr/ret subroutines are not supported",
|
||||
)),
|
||||
Insn::TableSwitch { .. } | Insn::LookupSwitch { .. } => {
|
||||
pop_slots(state, 1, class, method, "switch")?;
|
||||
|
|
@ -192,7 +196,7 @@ pub(super) fn transfer(
|
|||
ArrayKind::Int => "[I",
|
||||
ArrayKind::Long => "[J",
|
||||
ArrayKind::Reference => {
|
||||
return Err(invalid(class, method, "newarray ссылочного типа"));
|
||||
return Err(invalid(class, method, "newarray of a reference type"));
|
||||
}
|
||||
};
|
||||
push(
|
||||
|
|
@ -202,7 +206,16 @@ pub(super) fn transfer(
|
|||
}
|
||||
Insn::ANewArray(name) => {
|
||||
pop_slots(state, 1, class, method, "anewarray")?;
|
||||
push(state, vec![VerificationType::Object(format!("[L{name};"))])
|
||||
// `anewarray` adds one dimension: a plain class becomes
|
||||
// `[L...;`, an already-array operand (`anewarray [B` for a
|
||||
// partially bound multi-dim) gains one more `[`. Wrapping an
|
||||
// array operand in `L...;` (`[L[B;`) is not a class.
|
||||
let descriptor = if name.starts_with('[') {
|
||||
format!("[{name}")
|
||||
} else {
|
||||
format!("[L{name};")
|
||||
};
|
||||
push(state, vec![VerificationType::Object(descriptor)])
|
||||
}
|
||||
Insn::MultiANewArray { descriptor, dims } => {
|
||||
pop_slots(state, *dims as usize, class, method, "multianewarray")?;
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@ use super::State;
|
|||
use crate::error::ClassfileError;
|
||||
use crate::insn::SlotKind;
|
||||
|
||||
/// Запись в local с учётом ширины (`long`/`double` — парой с `Top`).
|
||||
/// Запись в local с учётом ширины (`long`/`double` - парой с `Top`).
|
||||
pub(super) fn store_local(
|
||||
state: &mut State,
|
||||
slot: u16,
|
||||
|
|
@ -38,7 +38,7 @@ pub(super) fn push(state: &mut State, slots: Vec<VerificationType>) -> Result<()
|
|||
Ok(())
|
||||
}
|
||||
|
||||
/// Снять `n` слотов (снизу вверх); нехватка — честная ошибка.
|
||||
/// Снять `n` слотов (снизу вверх); нехватка - честная ошибка.
|
||||
pub(super) fn pop_slots(
|
||||
state: &mut State,
|
||||
n: usize,
|
||||
|
|
@ -84,7 +84,7 @@ pub(super) fn local_set(
|
|||
Ok(())
|
||||
}
|
||||
|
||||
/// Значение local'а целиком: `long`/`double` — парой с `Top`.
|
||||
/// Значение local'а целиком: `long`/`double` - парой с `Top`.
|
||||
pub(super) fn wide_push(value: VerificationType) -> Vec<VerificationType> {
|
||||
match value {
|
||||
VerificationType::Long | VerificationType::Double => vec![value, VerificationType::Top],
|
||||
|
|
@ -112,7 +112,7 @@ pub(super) fn dup(
|
|||
push(state, out)
|
||||
}
|
||||
|
||||
/// `dup2*`: форма выбирается по вершине (`Top` — вторая половина `long`).
|
||||
/// `dup2*`: форма выбирается по вершине (`Top` - вторая половина `long`).
|
||||
pub(super) fn dup2(
|
||||
state: &mut State,
|
||||
class: &str,
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@
|
|||
//! `BootstrapMethods`, `EnclosingMethod`, `MethodParameters` and `ConstantValue`
|
||||
//! are modeled explicitly (they hold pool references that survive rebuilding);
|
||||
//! everything else that is structured but unneeded by us goes to `Raw` as-is.
|
||||
//! `Code` and `StackMapTable` are not handled here — the calling method code
|
||||
//! `Code` and `StackMapTable` are not handled here - the calling method code
|
||||
//! parses them (see `super::class`).
|
||||
|
||||
use ristretto_classfile::ConstantPool;
|
||||
|
|
@ -131,7 +131,7 @@ fn inner(
|
|||
})
|
||||
}
|
||||
|
||||
/// The enclosing class's method (`0` — class only, no method).
|
||||
/// The enclosing class's method (`0` - class only, no method).
|
||||
fn enclosing_method(
|
||||
pool: &ConstantPool<'_>,
|
||||
index: u16,
|
||||
|
|
|
|||
|
|
@ -11,7 +11,7 @@ use super::super::pool::class_name;
|
|||
use crate::error::ClassfileError;
|
||||
use crate::format::{StackMapFrame, VerificationType};
|
||||
|
||||
/// Backend frames → ours (index deltas — as-is).
|
||||
/// Backend frames → ours (index deltas - as-is).
|
||||
pub(super) fn to_frames(
|
||||
frames: &[StackFrame],
|
||||
pool: &ConstantPool<'_>,
|
||||
|
|
@ -100,7 +100,7 @@ fn vtype(value: &RtType, pool: &ConstantPool<'_>) -> Result<VerificationType, Cl
|
|||
})
|
||||
}
|
||||
|
||||
/// The catch type (`0` — catch-all for `finally`/`any`).
|
||||
/// The catch type (`0` - catch-all for `finally`/`any`).
|
||||
pub(super) fn catch_type(
|
||||
pool: &ConstantPool<'_>,
|
||||
index: u16,
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
//! Method-body labels: where they must stand and how they resolve.
|
||||
//!
|
||||
//! The backend numbers instructions in order — labels are placed directly on the
|
||||
//! The backend numbers instructions in order - labels are placed directly on the
|
||||
//! numbers, without byte offsets.
|
||||
|
||||
use std::collections::{BTreeMap, BTreeSet};
|
||||
|
|
@ -13,7 +13,7 @@ use crate::insn::Label;
|
|||
|
||||
/// Instruction numbers that must carry a label.
|
||||
///
|
||||
/// `extra` — debug-table positions (already as numbers): pass insertions must
|
||||
/// `extra` - debug-table positions (already as numbers): pass insertions must
|
||||
/// not leave them without a label.
|
||||
pub(super) fn label_indexes(
|
||||
code: &[Instruction],
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
//! Backend `Code` → our IR method body: labels, exceptions, frames.
|
||||
//!
|
||||
//! The backend already turned byte offsets into instruction indices — the
|
||||
//! The backend already turned byte offsets into instruction indices - the
|
||||
//! converters work directly with them: labels are placed on branch targets,
|
||||
//! try-block boundaries and debug-table positions; frames are carried over as-is
|
||||
//! (index deltas). `LineNumberTable`/`LocalVariableTable` positions are labels
|
||||
|
|
|
|||
|
|
@ -7,7 +7,7 @@ use super::super::pool::const_from;
|
|||
use crate::error::ClassfileError;
|
||||
use crate::insn::{Arith, ArrayKind, Const, Convert, Insn, Prim, SlotKind};
|
||||
|
||||
/// `Some` — the instruction is covered; `None` — hand it to the next converters.
|
||||
/// `Some` - the instruction is covered; `None` - hand it to the next converters.
|
||||
pub(super) fn convert_basic(
|
||||
instruction: &Instruction,
|
||||
pool: &ConstantPool<'_>,
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@ use super::Resolver;
|
|||
use crate::error::ClassfileError;
|
||||
use crate::insn::{BranchCond, Cond, Insn, Label, Prim};
|
||||
|
||||
/// `Some` — the instruction is covered; `None` — hand it to the next converters.
|
||||
/// `Some` - the instruction is covered; `None` - hand it to the next converters.
|
||||
pub(super) fn convert_flow(
|
||||
instruction: &Instruction,
|
||||
index: u16,
|
||||
|
|
@ -52,7 +52,7 @@ fn branch(cond: BranchCond, target: u16, resolve: &Resolver<'_>) -> Result<Insn,
|
|||
Ok(Insn::Branch(cond, resolve(target)?))
|
||||
}
|
||||
|
||||
/// A `switch` target — an instruction number: base plus the backend's relative delta.
|
||||
/// A `switch` target - an instruction number: base plus the backend's relative delta.
|
||||
fn abs(index: u16, delta: i32, resolve: &Resolver<'_>) -> Result<Label, ClassfileError> {
|
||||
let absolute = i32::from(index)
|
||||
.checked_add(delta)
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@
|
|||
//!
|
||||
//! Branch offsets come from the backend (absolute for `if*`/`goto`, relative to
|
||||
//! the instruction start for `switch`), so conversion takes a resolver from an
|
||||
//! offset to a [`Label`](crate::insn::Label) — the calling code assigns labels.
|
||||
//! offset to a [`Label`](crate::insn::Label) - the calling code assigns labels.
|
||||
|
||||
mod basic;
|
||||
mod flow;
|
||||
|
|
@ -17,7 +17,7 @@ use crate::insn::{Insn, Label};
|
|||
/// Resolves an absolute bytecode offset into a label.
|
||||
pub(super) type Resolver<'a> = dyn Fn(u16) -> Result<Label, ClassfileError> + 'a;
|
||||
|
||||
/// Convert one instruction. `index` — its number in the method body (`switch` needs it).
|
||||
/// Convert one instruction. `index` - its number in the method body (`switch` needs it).
|
||||
pub(super) fn convert(
|
||||
instruction: &Instruction,
|
||||
index: u16,
|
||||
|
|
|
|||
|
|
@ -7,7 +7,7 @@ use super::super::pool::{class_name, dynamic_parts, expect_kind, is_interface, r
|
|||
use crate::error::ClassfileError;
|
||||
use crate::insn::{ArrayKind, Insn, InvokeKind};
|
||||
|
||||
/// `Some` — the instruction is covered; `None` — hand it to the next converters.
|
||||
/// `Some` - the instruction is covered; `None` - hand it to the next converters.
|
||||
pub(super) fn convert_ref(
|
||||
instruction: &Instruction,
|
||||
pool: &ConstantPool<'_>,
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
//! Our IR attributes → backend attributes.
|
||||
//!
|
||||
//! Attribute names (`SourceFile`, …) are interned into the pool right here — the
|
||||
//! Attribute names (`SourceFile`, …) are interned into the pool right here - the
|
||||
//! backend stores only `name_index`, and the string is recovered on read.
|
||||
|
||||
use ristretto_classfile::attributes::{
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@
|
|||
//!
|
||||
//! Our IR stores `offset_delta` in instruction numbers; the backend recomputes
|
||||
//! them into bytes on `to_bytes`. The compact form (`Same`/`SameLocals`) is valid
|
||||
//! only when the **byte** delta is ≤ 63 — otherwise the extended form is needed.
|
||||
//! only when the **byte** delta is ≤ 63 - otherwise the extended form is needed.
|
||||
//! We choose by the byte delta: bytes are computed from the current method's layout.
|
||||
|
||||
use ristretto_classfile::attributes::{StackFrame as RtFrame, VerificationType as RtType};
|
||||
|
|
@ -14,7 +14,7 @@ use crate::format::{StackMapFrame, VerificationType};
|
|||
/// The byte offset of an instruction's start (for computing frame deltas).
|
||||
type ByteOf<'a> = &'a (dyn Fn(u32) -> Result<u16, ClassfileError> + 'a);
|
||||
|
||||
/// IR frames → backend frames (index deltas — as-is).
|
||||
/// IR frames → backend frames (index deltas - as-is).
|
||||
pub(super) fn convert_frames(
|
||||
frames: &[StackMapFrame],
|
||||
pool: &mut Pool,
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
//! Our [`Insn`](crate::insn::Insn) → the backend's `Instruction`.
|
||||
//!
|
||||
//! The reverse side of `super::super::parse::insns`: labels resolve into
|
||||
//! absolute instruction numbers (`switch` — into deltas from itself), constants
|
||||
//! absolute instruction numbers (`switch` - into deltas from itself), constants
|
||||
//! and references go to the pool via [`Pool`](super::super::pool::Pool).
|
||||
|
||||
use indexmap::IndexMap;
|
||||
|
|
@ -13,9 +13,9 @@ use super::{branch, delta, fit_index, ret_op};
|
|||
use crate::error::ClassfileError;
|
||||
use crate::insn::{Insn, Label, SlotKind};
|
||||
|
||||
/// One IR instruction → a backend instruction (`None` — a label marker).
|
||||
/// One IR instruction → a backend instruction (`None` - a label marker).
|
||||
///
|
||||
/// `index` — the instruction's number in the method (needed for `switch` deltas).
|
||||
/// `index` - the instruction's number in the method (needed for `switch` deltas).
|
||||
pub(crate) fn convert(
|
||||
insn: &Insn,
|
||||
index: u32,
|
||||
|
|
|
|||
|
|
@ -132,7 +132,7 @@ pub(crate) fn measure(insns: &[RtInsn]) -> Result<BTreeMap<u32, u16>, ClassfileE
|
|||
Ok(map)
|
||||
}
|
||||
|
||||
/// Branch targets don't affect size — substitute the current position for sizing.
|
||||
/// Branch targets don't affect size - substitute the current position for sizing.
|
||||
fn smash_targets(insn: &mut RtInsn, position: u16) {
|
||||
match insn {
|
||||
RtInsn::Ifeq(target)
|
||||
|
|
|
|||
|
|
@ -229,7 +229,7 @@ fn arg_count(descriptor: &str) -> Result<u8, ClassfileError> {
|
|||
.map_err(|_| ClassfileError::Write(format!("слишком много аргументов в {descriptor}")))
|
||||
}
|
||||
|
||||
/// Skip characters up to the separator (inclusive); `false` — the string ended.
|
||||
/// Skip characters up to the separator (inclusive); `false` - the string ended.
|
||||
fn skip_to(chars: &mut std::str::Chars<'_>, end: char) -> bool {
|
||||
for char in chars {
|
||||
if char == end {
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
//! Helpers building `InnerClasses`/`BootstrapMethods`/`MethodParameters`.
|
||||
//!
|
||||
//! Moved out of `attrs` so it stays within the 250-line limit; the logic is
|
||||
//! unchanged — only the location. Each builds a backend entry from an IR
|
||||
//! unchanged - only the location. Each builds a backend entry from an IR
|
||||
//! element, interning names/descriptors and resolving `CONSTANT_Class` indices
|
||||
//! through the pool.
|
||||
|
||||
|
|
|
|||
|
|
@ -2,10 +2,10 @@
|
|||
//!
|
||||
//! The pool is built from scratch by a deterministic walk (classes, fields,
|
||||
//! methods, attributes, code), so identical IR yields identical bytes. `Raw`
|
||||
//! attribute indices are not carried over — see `super::attrs`.
|
||||
//! attribute indices are not carried over - see `super::attrs`.
|
||||
//!
|
||||
//! The backend does not deduplicate: `add_*` always pushes a new entry. We cache
|
||||
//! semantic keys ourselves — otherwise `LocalVariableTable` and
|
||||
//! semantic keys ourselves - otherwise `LocalVariableTable` and
|
||||
//! `LocalVariableTypeTable` would get different `name_index` values for the same
|
||||
//! name, and HotSpot would reject the class (`LVTT entry ... does not match any
|
||||
//! LVT entry`). The walk order is deterministic, so the cache only changes pool
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@
|
|||
//!
|
||||
//! This is an `impl Pool` from [`super`], split out so the parent file stays
|
||||
//! within the structure limit. Each builder is memoized by a semantic key
|
||||
//! (owner/name/descriptor/…) — an identical reference always gets the same index,
|
||||
//! (owner/name/descriptor/…) - an identical reference always gets the same index,
|
||||
//! and the pool doesn't bloat with duplicates (bloat would otherwise widen `ldc`
|
||||
//! into `ldc_w` and change code length on relocation).
|
||||
|
||||
|
|
|
|||
|
|
@ -88,7 +88,7 @@ pub enum InvokeKind {
|
|||
Interface,
|
||||
}
|
||||
|
||||
/// Type conversion (`i2l`, `f2b`, ... — JVMS §6.5.i2<b|c|s|l|f|d>).
|
||||
/// Type conversion (`i2l`, `f2b`, ... - JVMS §6.5.i2<b|c|s|l|f|d>).
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum Convert {
|
||||
I2L,
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
//! IR instructions: no offsets, branches address a [`Label`].
|
||||
//!
|
||||
//! This is fundamental: any pass that changes instruction order/length need not
|
||||
//! recompute jump offsets — the writer does that once at the end of the pipeline.
|
||||
//! recompute jump offsets - the writer does that once at the end of the pipeline.
|
||||
|
||||
mod kinds;
|
||||
|
||||
|
|
@ -27,7 +27,7 @@ pub enum Insn {
|
|||
slot: u16,
|
||||
kind: SlotKind,
|
||||
},
|
||||
/// `iinc` — increase a local variable by a constant.
|
||||
/// `iinc` - increase a local variable by a constant.
|
||||
Iinc {
|
||||
index: u16,
|
||||
delta: i32,
|
||||
|
|
@ -45,7 +45,7 @@ pub enum Insn {
|
|||
Dup2X1,
|
||||
Dup2X2,
|
||||
Swap,
|
||||
/// `iadd`, `lmul`, `lneg`, ... — type + operation.
|
||||
/// `iadd`, `lmul`, `lneg`, ... - type + operation.
|
||||
Binary {
|
||||
prim: Prim,
|
||||
op: Arith,
|
||||
|
|
@ -59,9 +59,9 @@ pub enum Insn {
|
|||
Convert(Convert),
|
||||
Branch(BranchCond, Label),
|
||||
Goto(Label),
|
||||
/// `jsr` — subroutine call (only version ≤ 50 classes).
|
||||
/// `jsr` - subroutine call (only version ≤ 50 classes).
|
||||
Jsr(Label),
|
||||
/// `ret` — return from a subroutine (`jsr`/`ret` come in pairs).
|
||||
/// `ret` - return from a subroutine (`jsr`/`ret` come in pairs).
|
||||
Ret(u16),
|
||||
TableSwitch {
|
||||
default: Label,
|
||||
|
|
@ -73,9 +73,9 @@ pub enum Insn {
|
|||
default: Label,
|
||||
pairs: Vec<(i32, Label)>,
|
||||
},
|
||||
/// `return`/`ireturn`/...; `None` — void return.
|
||||
/// `return`/`ireturn`/...; `None` - void return.
|
||||
Return(Option<Prim>),
|
||||
/// `areturn` — return a reference ([`Prim`] has no reference type).
|
||||
/// `areturn` - return a reference ([`Prim`] has no reference type).
|
||||
AReturn,
|
||||
GetStatic {
|
||||
owner: String,
|
||||
|
|
@ -112,7 +112,7 @@ pub enum Insn {
|
|||
New(String),
|
||||
NewArray(ArrayKind),
|
||||
ANewArray(String),
|
||||
/// `multianewarray` — array of any dimension; `dims` — how many dimensions from the stack.
|
||||
/// `multianewarray` - array of any dimension; `dims` - how many dimensions from the stack.
|
||||
MultiANewArray {
|
||||
descriptor: String,
|
||||
dims: u8,
|
||||
|
|
|
|||
|
|
@ -27,3 +27,11 @@ pub use model::{
|
|||
LocalVariable, LocalVariableType, Method, MethodParameter, Program, RecordComponent, Resource,
|
||||
Version,
|
||||
};
|
||||
|
||||
/// Safety budget: a method body that would grow past this many instructions
|
||||
/// is left alone by the expanding passes (`strings`, `constants`).
|
||||
///
|
||||
/// The writer caps a method body at 64KB; at a pessimistic 8 bytes per
|
||||
/// instruction this count can never overflow it, while real methods
|
||||
/// (hundreds of instructions) never come close.
|
||||
pub const MAX_METHOD_INSNS: usize = 8_000;
|
||||
|
|
|
|||
|
|
@ -17,7 +17,7 @@ pub struct ClassBuilder {
|
|||
|
||||
impl ClassBuilder {
|
||||
/// A new `public` class with the given superclass (`None` = `java/lang/Object`,
|
||||
/// except for `java/lang/Object` itself — it has no superclass).
|
||||
/// except for `java/lang/Object` itself - it has no superclass).
|
||||
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 {
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
//! The "program" structure — the unit of work for the pipeline.
|
||||
//! The "program" structure - the unit of work for the pipeline.
|
||||
|
||||
pub mod builder;
|
||||
pub mod version;
|
||||
|
|
@ -8,7 +8,7 @@ pub use version::Version;
|
|||
use crate::format::StackMapFrame;
|
||||
use crate::insn::{Const, Insn, Label};
|
||||
|
||||
/// Unknown/specific attributes are stored as bytes — lossless round-trip.
|
||||
/// Unknown/specific attributes are stored as bytes - lossless round-trip.
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
pub enum Attribute {
|
||||
SourceFile(String),
|
||||
|
|
@ -32,7 +32,7 @@ pub enum Attribute {
|
|||
class: String,
|
||||
method: Option<(String, String)>,
|
||||
},
|
||||
/// `MethodParameters`: formal parameter names (`None` — unnamed).
|
||||
/// `MethodParameters`: formal parameter names (`None` - unnamed).
|
||||
MethodParameters(Vec<MethodParameter>),
|
||||
/// `Record`: the record class's components.
|
||||
Record(Vec<RecordComponent>),
|
||||
|
|
@ -47,7 +47,7 @@ pub enum Attribute {
|
|||
|
||||
#[derive(Debug, Clone, Copy, PartialEq)]
|
||||
pub struct LineNumber {
|
||||
/// Start label (the writer computes bytes — survives pass insertions).
|
||||
/// Start label (the writer computes bytes - survives pass insertions).
|
||||
pub start: Label,
|
||||
pub line: u16,
|
||||
}
|
||||
|
|
@ -159,7 +159,7 @@ impl Code {
|
|||
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
pub struct ExceptionHandler {
|
||||
/// `None` — catch-all (`finally`).
|
||||
/// `None` - catch-all (`finally`).
|
||||
pub start: Label,
|
||||
pub end: Label,
|
||||
pub handler: Label,
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@
|
|||
//! Frames are dropped and recomputed. Judges: determinism + IR round-trip +
|
||||
//! real `java -Xverify:all` for classes without external dependencies.
|
||||
//! (The backend's `verify_class` is unfit for acceptance: it expects frames
|
||||
//! in bytes while `from_bytes` hands out indices — see `todo.md`.)
|
||||
//! in bytes while `from_bytes` hands out indices - see `todo.md`.)
|
||||
|
||||
#![allow(clippy::expect_used, clippy::unwrap_used)]
|
||||
|
||||
|
|
@ -28,7 +28,7 @@ impl Hierarchy for MapHierarchy {
|
|||
}
|
||||
}
|
||||
|
||||
/// JDK-only classes — `java` checks them fully.
|
||||
/// JDK-only classes - `java` checks them fully.
|
||||
const JDK_ONLY: [&str; 2] = [
|
||||
"dev/loki/lorep/util/TimeUtil",
|
||||
"dev/loki/lorep/util/PaginationUtil",
|
||||
|
|
@ -95,7 +95,7 @@ fn fixtures_recompute_and_verify() {
|
|||
/// A join with a `double` below on the stack must emit it as ONE entry.
|
||||
///
|
||||
/// Regression: the emitter used to write the slot pair (`[Double, Top]`),
|
||||
/// like `javac` does for locals — but on the operand stack `javac` writes a
|
||||
/// like `javac` does for locals - but on the operand stack `javac` writes a
|
||||
/// bare `[Double]`, and HotSpot rejects the pair with
|
||||
/// `Inconsistent stackmap frames`.
|
||||
#[test]
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@
|
|||
//! сопоставляет запись `LocalVariableTypeTable` с `LocalVariableTable` по
|
||||
//! сырому `name_index`. Дублирующий `Utf8` для одного имени даёт разные индексы
|
||||
//! и рушит класс: `ClassFormatError: LVTT entry ... does not match any LVT
|
||||
//! entry`. Писатель обязан интернировать строки — поэтому в записанном пуле
|
||||
//! entry`. Писатель обязан интернировать строки - поэтому в записанном пуле
|
||||
//! каждый `CONSTANT_Utf8` уникален.
|
||||
|
||||
#![allow(clippy::expect_used, clippy::unwrap_used)]
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@
|
|||
//!
|
||||
//! Пул при записи строится заново (порядок другой), поэтому сравнивается IR,
|
||||
//! а не байты. Записанные байты обязаны проходить структурный `verify`
|
||||
//! backend'а — это ловит битые ссылки пула и кривые атрибуты.
|
||||
//! backend'а - это ловит битые ссылки пула и кривые атрибуты.
|
||||
|
||||
#![allow(clippy::expect_used, clippy::unwrap_used)]
|
||||
|
||||
|
|
|
|||
|
|
@ -1,8 +1,8 @@
|
|||
//! Spike, часть 2: доступна ли **генерация** кадров `StackMapTable`.
|
||||
//!
|
||||
//! Ключевой вопрос `docs/stack.md` §3: считает ли `ristretto_classfile` кадры
|
||||
//! сам и отдаёт ли их наружу. Если да — наша работа сводится к записи; если
|
||||
//! нет — кадры придётся считать (или вендорить их `verifiers::bytecode`).
|
||||
//! сам и отдаёт ли их наружу. Если да - наша работа сводится к записи; если
|
||||
//! нет - кадры придётся считать (или вендорить их `verifiers::bytecode`).
|
||||
|
||||
#![allow(clippy::expect_used, clippy::unwrap_used)]
|
||||
|
||||
|
|
@ -82,7 +82,7 @@ fn inference_config() -> VerifierConfig {
|
|||
VerifierConfig::default().with_fallback_strategy(FallbackStrategy::AlwaysInference)
|
||||
}
|
||||
|
||||
/// Без `StackMapTable` inference обязан пройти сам — значит, движок кадров у
|
||||
/// Без `StackMapTable` inference обязан пройти сам - значит, движок кадров у
|
||||
/// крейта есть, просто его результат не отдаётся наружу.
|
||||
#[test]
|
||||
fn inference_path_verifies_without_stack_map_table() {
|
||||
|
|
@ -94,7 +94,7 @@ fn inference_path_verifies_without_stack_map_table() {
|
|||
assert_eq!(results[0].path_used, VerificationPath::Inference);
|
||||
}
|
||||
|
||||
/// Строгий режим без кадров обязан упасть — иначе требование `StackMapTable`
|
||||
/// Строгий режим без кадров обязан упасть - иначе требование `StackMapTable`
|
||||
/// для классов ≥ 50 не проверяется вовсе.
|
||||
#[test]
|
||||
fn strict_mode_rejects_missing_stack_map_table() {
|
||||
|
|
@ -102,7 +102,7 @@ fn strict_mode_rejects_missing_stack_map_table() {
|
|||
let config = VerifierConfig::default().with_fallback_strategy(FallbackStrategy::Strict);
|
||||
let outcome = verify_class(&class_file, &Permissive, &config);
|
||||
|
||||
// Строгий режим падает либо `Err`, либо `success == false` — оба варианта
|
||||
// Строгий режим падает либо `Err`, либо `success == false` - оба варианта
|
||||
// приемлемы, важен сам отказ.
|
||||
let rejected = match &outcome {
|
||||
Err(_) => true,
|
||||
|
|
@ -113,13 +113,13 @@ fn strict_mode_rejects_missing_stack_map_table() {
|
|||
|
||||
/// Негативный результат spike'а: кадр не на том сдвиге не отдаёт диагностику.
|
||||
///
|
||||
/// Ждали `Ok(vec![result])` с заполненным `diagnostic.expected` — тогда
|
||||
/// Ждали `Ok(vec![result])` с заполненным `diagnostic.expected` - тогда
|
||||
/// генерация кадров шла бы итеративным путём через публичный API. Фактически
|
||||
/// `verify_class` в строгом режиме возвращает `Err(VerifyError)`, и ожидаемый
|
||||
/// кадр до нас не доходит: из крейта достаётся только место ошибки
|
||||
/// («merge point at offset N»), но не её содержимое.
|
||||
///
|
||||
/// Вывод для `docs/stack.md` §3: проверять кадры крейт умеет, отдавать — нет.
|
||||
/// Вывод для `docs/stack.md` §3: проверять кадры крейт умеет, отдавать - нет.
|
||||
#[test]
|
||||
fn diagnostic_exposes_expected_frame_on_failure() {
|
||||
let mut class_file = branch_class();
|
||||
|
|
@ -127,7 +127,7 @@ fn diagnostic_exposes_expected_frame_on_failure() {
|
|||
.constant_pool
|
||||
.add_utf8("StackMapTable")
|
||||
.expect("utf8");
|
||||
// Кадр на сдвиге 0, а настоящий якорь — на сдвиге 3.
|
||||
// Кадр на сдвиге 0, а настоящий якорь - на сдвиге 3.
|
||||
class_file.attributes.push(Attribute::StackMapTable {
|
||||
name_index,
|
||||
frames: vec![StackFrame::SameFrame { frame_type: 0 }],
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
//! Spike: пригоден ли `ristretto_classfile` как backend для чтения/записи.
|
||||
//!
|
||||
//! Критерии — `docs/stack.md` §2. Тест существует, чтобы ответы на вопросы
|
||||
//! Критерии - `docs/stack.md` §2. Тест существует, чтобы ответы на вопросы
|
||||
//! spike'а не устаревали: если крейт сломает хотя бы один пункт, сборка упадёт,
|
||||
//! и решение о backend'е придётся пересмотреть.
|
||||
//!
|
||||
|
|
@ -59,7 +59,7 @@ fn round_trip_preserves_identity() {
|
|||
assert_eq!(restored.constant_pool.len(), source.constant_pool.len());
|
||||
}
|
||||
|
||||
/// Кадры `StackMapTable` конструируются и сериализуются публичным API —
|
||||
/// Кадры `StackMapTable` конструируются и сериализуются публичным API -
|
||||
/// значит, мы можем **записывать** кадры, а не только читать.
|
||||
#[test]
|
||||
fn stack_map_frames_are_constructible() {
|
||||
|
|
|
|||
|
|
@ -61,7 +61,7 @@ pub enum Profile {
|
|||
Standard,
|
||||
/// Всё включено, включая агрессивный control-flow.
|
||||
Paranoid,
|
||||
/// Ничего не включено явно — только `enable` из конфига.
|
||||
/// Ничего не включено явно - только `enable` из конфига.
|
||||
Manual,
|
||||
}
|
||||
|
||||
|
|
@ -97,7 +97,7 @@ impl Profile {
|
|||
#[serde(default, deny_unknown_fields)]
|
||||
pub struct Config {
|
||||
pub profile: Profile,
|
||||
/// Мастер-сид; `None` — случайный, но обязательно попадает в отчёт.
|
||||
/// Мастер-сид; `None` - случайный, но обязательно попадает в отчёт.
|
||||
pub seed: Option<u64>,
|
||||
/// Принудительно включить пассы вне профиля.
|
||||
pub enable: Vec<PassId>,
|
||||
|
|
|
|||
|
|
@ -6,9 +6,9 @@ use serde::{Deserialize, Serialize};
|
|||
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "kebab-case")]
|
||||
pub enum NameStyle {
|
||||
/// `a`, `b`, ... `aa` — short Latin.
|
||||
/// `a`, `b`, ... `aa` - short Latin.
|
||||
ShortLatin,
|
||||
/// `I`, `l`, `O`, `0` — visually identical.
|
||||
/// `I`, `l`, `O`, `0` - visually identical.
|
||||
IlO0,
|
||||
/// Random Unicode names (surrogates are valid in modified UTF-8).
|
||||
Unicode,
|
||||
|
|
@ -17,7 +17,7 @@ pub enum NameStyle {
|
|||
MixedLatin,
|
||||
/// Pseudo-meaningful words from another project (misdirection).
|
||||
DecoyWords,
|
||||
/// Plausible names: `TextureCache`, `resolveToken` — blends in as
|
||||
/// Plausible names: `TextureCache`, `resolveToken` - blends in as
|
||||
/// ordinary application code instead of obvious noise.
|
||||
Plausible,
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@
|
|||
//! Ядро обфускатора: конфигурация, детерминированный seed, маппинги,
|
||||
//! контекст прогона и контракт пассов ([`pipeline::Pass`]).
|
||||
//!
|
||||
//! Ядро ничего не знает о бинарном формате `.class` и о ZIP — только о модели
|
||||
//! Ядро ничего не знает о бинарном формате `.class` и о ZIP - только о модели
|
||||
//! [`noxis_classfile::Program`]. Сборка конвейера из пассов живёт в
|
||||
//! `noxis-passes`, чтобы не было цикла зависимостей.
|
||||
|
||||
|
|
|
|||
|
|
@ -17,7 +17,7 @@ pub fn field_key(owner: &str, name: &str, descriptor: &str) -> String {
|
|||
/// Таблицы «оригинал → обфусцированное».
|
||||
///
|
||||
/// Нужна по двум причинам: (1) перевод stack trace'ей обратно в читаемые имена
|
||||
/// (аналог ZKM Stack Trace Translate), (2) аудит — что именно поменялось.
|
||||
/// (аналог ZKM Stack Trace Translate), (2) аудит - что именно поменялось.
|
||||
#[derive(Debug, Clone, Default, PartialEq, Serialize)]
|
||||
pub struct Mapping {
|
||||
/// internal name класса → новый internal name
|
||||
|
|
|
|||
|
|
@ -31,7 +31,7 @@ impl Context {
|
|||
/// Детерминированный RNG для домена (`"rename"`, `"strings/0"`, ...).
|
||||
///
|
||||
/// Доменное разделение: разные пассы не должны вытягивать одну
|
||||
/// последовательность — иначе их «случайности» статистически коррелируют.
|
||||
/// последовательность - иначе их «случайности» статистически коррелируют.
|
||||
pub fn rng(&self, domain: &str) -> Rng {
|
||||
Rng::from_seed(derive_seed(self.master_seed, domain, 0))
|
||||
}
|
||||
|
|
|
|||
|
|
@ -46,7 +46,7 @@ pub trait Pass: Send + Sync {
|
|||
|
||||
/// Упорядоченная цепочка пассов.
|
||||
///
|
||||
/// Порядок — не деталь: см. `docs/architecture.md` (rename → resources →
|
||||
/// Порядок - не деталь: см. `docs/architecture.md` (rename → resources →
|
||||
/// strings → constants → flow → noise → debug), иначе часть техник ломает
|
||||
/// предыдущие или работает по уже мёртвым именам.
|
||||
#[derive(Default)]
|
||||
|
|
@ -88,7 +88,7 @@ impl Pipeline {
|
|||
for pass in &self.passes {
|
||||
let id = pass.id();
|
||||
if !ctx.config.is_enabled(id) {
|
||||
tracing::info!(target: "noxis", pass = %id, "пасс выключен — пропущен");
|
||||
tracing::info!(target: "noxis", pass = %id, "пасс выключен - пропущен");
|
||||
continue;
|
||||
}
|
||||
tracing::info!(target: "noxis", pass = %id, "пасс запущен");
|
||||
|
|
|
|||
|
|
@ -1,8 +1,8 @@
|
|||
//! Итог прогона: пассы + счётчики — сериализуется в JSON.
|
||||
//! Итог прогона: пассы + счётчики - сериализуется в JSON.
|
||||
|
||||
use serde::Serialize;
|
||||
|
||||
/// Итог прогона: пассы + счётчики — сериализуется в JSON.
|
||||
/// Итог прогона: пассы + счётчики - сериализуется в JSON.
|
||||
#[derive(Debug, Clone, Default, Serialize)]
|
||||
pub struct Report {
|
||||
pub seed: u64,
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
//! Детерминированный PRNG и вывод производных seed'ов.
|
||||
//!
|
||||
//! Свой генератор, а не `rand`: обфускация должна быть **воспроизводимой между
|
||||
//! версиями инструмента** — алгоритм замирает в формате, а не в изменении API
|
||||
//! версиями инструмента** - алгоритм замирает в формате, а не в изменении API
|
||||
//! чужого крейта. Схема: SplitMix64 для инициализации + xoshiro256++ для генерации.
|
||||
|
||||
/// Генератор xoshiro256++.
|
||||
|
|
@ -97,7 +97,7 @@ impl Rng {
|
|||
}
|
||||
}
|
||||
|
||||
/// FNV-1a 64 — детерминированный вывод производного сида.
|
||||
/// FNV-1a 64 - детерминированный вывод производного сида.
|
||||
///
|
||||
/// Разделение доменов обязательно: пассы не должны делить одну и ту же
|
||||
/// последовательность (иначе переименование и шифрование «случайно»
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
//! Чтение и запись ZIP-архива JAR.
|
||||
//!
|
||||
//! Единственная задача модуля — переместить записи из/в вектор [`Entry`],
|
||||
//! Единственная задача модуля - переместить записи из/в вектор [`Entry`],
|
||||
//! сохранив порядок и не потеряв дубликатов молча. Всё, что связано с
|
||||
//! содержимым (`Program`, маппинги, подпись), живёт в соседних модулях.
|
||||
|
||||
|
|
@ -30,7 +30,7 @@ pub enum JarError {
|
|||
|
||||
/// Одна запись архива: имя + байты.
|
||||
///
|
||||
/// Порядок в `[`Entry`]` — это порядок в архиве, он сохраняется при
|
||||
/// Порядок в `[`Entry`]` - это порядок в архиве, он сохраняется при
|
||||
/// чтении и записи. Метод сжатия не хранится: всё пишется deflate'ом,
|
||||
/// что детерминированно и одинаково для одинаковых данных.
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
|
|
@ -51,7 +51,7 @@ impl Entry {
|
|||
|
||||
/// Читает все не-каталожные записи из `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)?;
|
||||
|
|
|
|||
|
|
@ -4,13 +4,13 @@
|
|||
//!
|
||||
//! Слой разделён на три несвязанные задачи:
|
||||
//!
|
||||
//! * [`archive`] — чистая работа с ZIP: порядок записей, сжатие, дубликаты;
|
||||
//! * [`signatures`] — срез невалидных подписей `META-INF/*.SF|RSA|DSA|EC`;
|
||||
//! * [`loader`] — перевод архива в [`Program`](noxis_classfile::Program)
|
||||
//! * [`archive`] - чистая работа с ZIP: порядок записей, сжатие, дубликаты;
|
||||
//! * [`signatures`] - срез невалидных подписей `META-INF/*.SF|RSA|DSA|EC`;
|
||||
//! * [`loader`] - перевод архива в [`Program`](noxis_classfile::Program)
|
||||
//! и обратно (классы через [`format`](noxis_classfile::format), остальное
|
||||
//! как ресурсы).
|
||||
//!
|
||||
//! Обфускация ресурсов — пасс `resources` в `noxis-passes`; сюда она не
|
||||
//! Обфускация ресурсов - пасс `resources` в `noxis-passes`; сюда она не
|
||||
//! входит, потому что ей нужен маппинг имён, а не I/O.
|
||||
|
||||
pub mod archive;
|
||||
|
|
|
|||
|
|
@ -1,9 +1,11 @@
|
|||
//! Перевод архива в [`Program`] и обратно.
|
||||
//! Archive to [`Program`] and back.
|
||||
//!
|
||||
//! Правило простое: `*.class` уходит в парсер, всё остальное — в ресурсы.
|
||||
//! Обратно классы сериализуются первыми, затем ресурсы: порядок внутри
|
||||
//! архива на поведение не влияет, а фиксированный порядок даёт
|
||||
//! воспроизводимую запись.
|
||||
//! The rule is simple: `*.class` goes to the parser, everything else -
|
||||
//! to resources. Classes serialize first, then resources: the order inside
|
||||
//! the archive does not affect behaviour, and a fixed order gives
|
||||
//! reproducible writes.
|
||||
|
||||
use std::collections::BTreeSet;
|
||||
|
||||
use noxis_classfile::Program;
|
||||
use noxis_classfile::format;
|
||||
|
|
@ -11,13 +13,22 @@ use noxis_classfile::model::Resource;
|
|||
|
||||
use crate::archive::{Entry, JarError};
|
||||
|
||||
/// Разбирает записи архива: классы — через парсер, остальное — как ресурсы.
|
||||
/// Parses archive entries: classes - through the parser, the rest - as resources.
|
||||
///
|
||||
/// A name that repeats (multi-release jars carry the same class at the root
|
||||
/// and under `META-INF/versions/*`) is kept once, first occurrence wins:
|
||||
/// the flat model cannot hold two classes under one name, and later passes
|
||||
/// (rename maps, jar writing) assume uniqueness.
|
||||
pub fn load(entries: Vec<Entry>) -> Result<Program, JarError> {
|
||||
let mut program = Program::default();
|
||||
let mut seen: BTreeSet<String> = BTreeSet::new();
|
||||
|
||||
for entry in entries {
|
||||
if is_class_entry(&entry.name) {
|
||||
program.classes.push(format::parse(&entry.data)?);
|
||||
let class = format::parse(&entry.data)?;
|
||||
if seen.insert(class.this_class.clone()) {
|
||||
program.classes.push(class);
|
||||
}
|
||||
} else {
|
||||
program.resources.push(Resource {
|
||||
path: entry.name,
|
||||
|
|
@ -29,7 +40,7 @@ pub fn load(entries: Vec<Entry>) -> Result<Program, JarError> {
|
|||
Ok(program)
|
||||
}
|
||||
|
||||
/// Собирает записи архива из [`Program`]: сначала классы, потом ресурсы.
|
||||
/// Assembles archive entries from [`Program`]: classes first, then resources.
|
||||
pub fn save(program: &Program) -> Result<Vec<Entry>, JarError> {
|
||||
let mut entries = Vec::with_capacity(program.classes.len() + program.resources.len());
|
||||
|
||||
|
|
@ -44,7 +55,7 @@ pub fn save(program: &Program) -> Result<Vec<Entry>, JarError> {
|
|||
Ok(entries)
|
||||
}
|
||||
|
||||
/// Классовый файл внутри архива: путь оканчивается на `.class`.
|
||||
/// A class file inside the archive: the path ends in `.class`.
|
||||
fn is_class_entry(name: &str) -> bool {
|
||||
name.len() > ".class".len() && name.ends_with(".class")
|
||||
}
|
||||
|
|
@ -78,7 +89,7 @@ mod tests {
|
|||
assert_eq!(save(&program).expect("save"), source);
|
||||
}
|
||||
|
||||
/// До P1 `format::parse` — заглушка; после P1 ожидается успешный разбор.
|
||||
/// Class entries go through the parser (round-trip keeps them intact).
|
||||
#[test]
|
||||
fn class_entries_go_through_the_parser() {
|
||||
let entries = vec![
|
||||
|
|
@ -97,4 +108,22 @@ mod tests {
|
|||
Err(other) => panic!("unexpected error: {other}"),
|
||||
}
|
||||
}
|
||||
|
||||
/// Multi-release twins (`demo/App.class` + `META-INF/versions/9/...`):
|
||||
/// the flat model keeps the first occurrence so no two classes ever
|
||||
/// share one name downstream.
|
||||
#[test]
|
||||
fn duplicate_class_names_keep_first() {
|
||||
use noxis_classfile::ClassBuilder;
|
||||
|
||||
let class = ClassBuilder::new("demo/App", None).build();
|
||||
let bytes = format::write(&class).expect("write");
|
||||
let entries = vec![
|
||||
Entry::new("demo/App.class", bytes.clone()),
|
||||
Entry::new("META-INF/versions/9/demo/App.class", bytes),
|
||||
];
|
||||
let program = load(entries).expect("load");
|
||||
assert_eq!(program.classes.len(), 1);
|
||||
assert_eq!(program.classes[0].this_class, "demo/App");
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@
|
|||
//!
|
||||
//! После перепаковки архива `META-INF/*.SF` и `*.RSA|DSA|EC` описывают
|
||||
//! **старые** байты: JVM либо отказывается запускать jar, либо ругается на
|
||||
//! подпись. Вырезать их обязательно — и это делается на уровне записей,
|
||||
//! подпись. Вырезать их обязательно - и это делается на уровне записей,
|
||||
//! потому что `MANIFEST.MF` остаётся, а его секции `Name:` тоже нужно
|
||||
//! выкинуть (ими занимается пасс `resources`).
|
||||
|
||||
|
|
@ -19,10 +19,10 @@ const META_INF: &str = "META-INF/";
|
|||
|
||||
/// Проверяет, является ли запись архива файлом подписи.
|
||||
///
|
||||
/// Совпадение — только для файлов, лежащих **непосредственно** в
|
||||
/// `META-INF/`: вложенный `META-INF/certs/x.SF` — обычный ресурс.
|
||||
/// Совпадение - только для файлов, лежащих **непосредственно** в
|
||||
/// `META-INF/`: вложенный `META-INF/certs/x.SF` - обычный ресурс.
|
||||
///
|
||||
/// Регистр не важен (`meta-inf/x.sf` — тоже подпись), `MANIFEST.MF`
|
||||
/// Регистр не важен (`meta-inf/x.sf` - тоже подпись), `MANIFEST.MF`
|
||||
/// никогда не является подписью.
|
||||
pub fn is_signature_entry(name: &str) -> bool {
|
||||
let Some(relative) = name.strip_prefix(META_INF) else {
|
||||
|
|
|
|||
|
|
@ -92,7 +92,7 @@ fn binary_long(first: i64, second: i64, op: Arith) -> Vec<Insn> {
|
|||
]
|
||||
}
|
||||
|
||||
/// `v = ((v >> 1) << 1) | (v & 1)` — works for any value, including negatives.
|
||||
/// `v = ((v >> 1) << 1) | (v & 1)` - works for any value, including negatives.
|
||||
fn shift_form(value: i32) -> Vec<Insn> {
|
||||
vec![
|
||||
Insn::Const(Const::Int(value >> 1)),
|
||||
|
|
@ -137,7 +137,7 @@ fn shift_form_long(value: i64) -> Vec<Insn> {
|
|||
pub(super) fn opaque_true(dead: u32, merge: u32, rng: &mut Rng) -> Vec<Insn> {
|
||||
let constant = rng.next_u64() as i32;
|
||||
vec![
|
||||
// P = ((C*C) + C) & 1 — even, so always 0.
|
||||
// P = ((C*C) + C) & 1 - even, so always 0.
|
||||
Insn::Const(Const::Int(constant)),
|
||||
Insn::Dup,
|
||||
Insn::Binary {
|
||||
|
|
@ -154,7 +154,7 @@ pub(super) fn opaque_true(dead: u32, merge: u32, rng: &mut Rng) -> Vec<Insn> {
|
|||
prim: Prim::Int,
|
||||
op: Arith::And,
|
||||
},
|
||||
// P != 0 never holds — the branch into DEAD is not taken, value popped.
|
||||
// P != 0 never holds - the branch into DEAD is not taken, value popped.
|
||||
Insn::Branch(BranchCond::Zero(Cond::Ne), dead),
|
||||
Insn::Goto(merge),
|
||||
Insn::Mark(dead),
|
||||
|
|
|
|||
|
|
@ -1,8 +1,8 @@
|
|||
//! Пасс шифрования констант: числа → арифметика, плюс opaque-предикаты.
|
||||
//! The constants pass: numbers become arithmetic, plus opaque predicates.
|
||||
//!
|
||||
//! `float`/`double` не трогаются (битовая точность через целые — отдельно).
|
||||
//! Мелочь из `iconst`-диапазона (`-1..=5`, `0L/1L`) остаётся как есть:
|
||||
//! её шифрование только раздувает горячий код.
|
||||
//! `float`/`double` are left alone (bit-exact paths through integers are
|
||||
//! separate). Trivia from the `iconst` range (`-1..=5`, `0L/1L`) stays as is:
|
||||
//! encrypting it only bloats hot code.
|
||||
|
||||
mod forms;
|
||||
|
||||
|
|
@ -10,12 +10,12 @@ mod forms;
|
|||
mod tests;
|
||||
|
||||
use noxis_analysis::ClassHierarchy;
|
||||
use noxis_classfile::{Const, Insn, Program, compute_frames};
|
||||
use noxis_classfile::{Const, Insn, MAX_METHOD_INSNS, Program, compute_frames};
|
||||
use noxis_core::{Context, CoreError, Pass, PassId, PassReport};
|
||||
|
||||
use forms::{Scheme, encrypt_int, encrypt_long, opaque_true};
|
||||
|
||||
/// Пасс шифрования констант.
|
||||
/// The constants encryption pass.
|
||||
#[derive(Debug, Default, Clone, Copy)]
|
||||
pub struct ConstantsPass;
|
||||
|
||||
|
|
@ -30,17 +30,18 @@ impl Pass for ConstantsPass {
|
|||
for name in &cfg.schemes {
|
||||
schemes.push(
|
||||
Scheme::parse(name)
|
||||
.ok_or_else(|| pass_error(format!("неизвестная схема `{name}`")))?,
|
||||
.ok_or_else(|| pass_error(format!("unknown scheme `{name}`")))?,
|
||||
);
|
||||
}
|
||||
if schemes.is_empty() {
|
||||
return Err(pass_error("нет схем шифрования"));
|
||||
return Err(pass_error("no encryption schemes"));
|
||||
}
|
||||
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();
|
||||
let mut oversized: Vec<(usize, usize)> = Vec::new();
|
||||
|
||||
for (ci, class) in program.classes.iter_mut().enumerate() {
|
||||
if cfg
|
||||
|
|
@ -55,25 +56,34 @@ impl Pass for ConstantsPass {
|
|||
let Some(code) = method.code.as_mut() else {
|
||||
continue;
|
||||
};
|
||||
let mut fresh = Vec::with_capacity(code.insns.len());
|
||||
let original = std::mem::take(&mut code.insns);
|
||||
let mut fresh = Vec::with_capacity(original.len());
|
||||
let mut touched = false;
|
||||
for insn in code.insns.drain(..) {
|
||||
for insn in &original {
|
||||
match insn {
|
||||
Insn::Const(Const::Int(value)) if !is_tiny_int(value) => {
|
||||
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));
|
||||
fresh.extend(encrypt_int(*value, scheme, &mut rng));
|
||||
replaced += 1;
|
||||
touched = true;
|
||||
}
|
||||
Insn::Const(Const::Long(value)) if value != 0 && value != 1 => {
|
||||
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));
|
||||
fresh.extend(encrypt_long(*value, scheme, &mut rng));
|
||||
replaced += 1;
|
||||
touched = true;
|
||||
}
|
||||
other => fresh.push(other),
|
||||
other => fresh.push(other.clone()),
|
||||
}
|
||||
}
|
||||
if fresh.len() > MAX_METHOD_INSNS {
|
||||
// Would risk the 64KB writer ceiling: keep the original
|
||||
// code untouched (predicates included) and count a skip.
|
||||
code.insns = original;
|
||||
skipped += 1;
|
||||
oversized.push((ci, mi));
|
||||
continue;
|
||||
}
|
||||
code.insns = fresh;
|
||||
if touched {
|
||||
code.max_stack = code.max_stack.saturating_add(2);
|
||||
|
|
@ -84,6 +94,7 @@ impl Pass for ConstantsPass {
|
|||
insert_predicates(
|
||||
program,
|
||||
&mut mutated,
|
||||
&oversized,
|
||||
cfg.max_predicates,
|
||||
&mut rng,
|
||||
&mut predicates,
|
||||
|
|
@ -96,15 +107,17 @@ impl Pass for ConstantsPass {
|
|||
}
|
||||
}
|
||||
|
||||
/// Мелочь, которую шифровать невыгодно (уже `iconst`).
|
||||
/// Trivia that is not worth encrypting (already `iconst`).
|
||||
fn is_tiny_int(value: i32) -> bool {
|
||||
(-1..=5).contains(&value)
|
||||
}
|
||||
|
||||
/// Opaque-предикаты в начало методов с кодом (не больше лимита на метод).
|
||||
/// Opaque predicates at the top of methods with code (at most the limit).
|
||||
/// Oversized methods stay fully untouched, predicates included.
|
||||
fn insert_predicates(
|
||||
program: &mut Program,
|
||||
mutated: &mut Vec<(usize, usize)>,
|
||||
oversized: &[(usize, usize)],
|
||||
max_predicates: usize,
|
||||
rng: &mut noxis_core::Rng,
|
||||
predicates: &mut usize,
|
||||
|
|
@ -116,6 +129,9 @@ fn insert_predicates(
|
|||
for ci in 0..classes {
|
||||
let methods = program.classes[ci].methods.len();
|
||||
for mi in 0..methods {
|
||||
if oversized.contains(&(ci, mi)) {
|
||||
continue;
|
||||
}
|
||||
let empty_or_missing = program.classes[ci].methods[mi]
|
||||
.code
|
||||
.as_ref()
|
||||
|
|
@ -123,7 +139,7 @@ fn insert_predicates(
|
|||
if empty_or_missing {
|
||||
continue;
|
||||
}
|
||||
let missing = || pass_error("метод пропал при вставке предиката");
|
||||
let missing = || pass_error("method vanished while inserting a predicate");
|
||||
let mut top = program.classes[ci]
|
||||
.methods
|
||||
.get(mi)
|
||||
|
|
@ -166,7 +182,7 @@ fn insert_predicates(
|
|||
Ok(())
|
||||
}
|
||||
|
||||
/// Пересчитать фреймы затронутого метода.
|
||||
/// Recompute the frames of a touched method.
|
||||
fn refresh_frames(
|
||||
program: &mut Program,
|
||||
ci: usize,
|
||||
|
|
@ -177,14 +193,14 @@ fn refresh_frames(
|
|||
let class = program
|
||||
.classes
|
||||
.get(ci)
|
||||
.ok_or_else(|| pass_error("класс пропал"))?;
|
||||
.ok_or_else(|| pass_error("class vanished"))?;
|
||||
(
|
||||
class.this_class.clone(),
|
||||
class
|
||||
.methods
|
||||
.get(mi)
|
||||
.cloned()
|
||||
.ok_or_else(|| pass_error("метод пропал"))?,
|
||||
.ok_or_else(|| pass_error("method vanished"))?,
|
||||
)
|
||||
};
|
||||
let frames =
|
||||
|
|
@ -194,7 +210,7 @@ fn refresh_frames(
|
|||
.get_mut(ci)
|
||||
.and_then(|c| c.methods.get_mut(mi))
|
||||
.and_then(|m| m.code.as_mut())
|
||||
.ok_or_else(|| pass_error("метод пропал"))?
|
||||
.ok_or_else(|| pass_error("method vanished"))?
|
||||
.frames = frames;
|
||||
Ok(())
|
||||
}
|
||||
|
|
@ -205,7 +221,7 @@ fn report(replaced: usize, predicates: usize, skipped: usize) -> PassReport {
|
|||
transformed: replaced + predicates,
|
||||
skipped,
|
||||
notes: vec![format!(
|
||||
"констант разбито: {replaced}, предикатов: {predicates}"
|
||||
"constants split: {replaced}, predicates: {predicates}"
|
||||
)],
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,14 +1,14 @@
|
|||
//! Тесты пасса констант: формы вычисляют исходник, мелочь цела.
|
||||
//! Constant-pass tests: forms compute the source value, trivia survives.
|
||||
|
||||
#![allow(clippy::expect_used, clippy::unwrap_used)]
|
||||
|
||||
use noxis_classfile::{ClassBuilder, Code, Const, Insn, Prim, Program, Version};
|
||||
use noxis_classfile::{ClassBuilder, Code, Const, Insn, MAX_METHOD_INSNS, Prim, Program, Version};
|
||||
use noxis_core::{Config, Context, Pass};
|
||||
|
||||
use super::ConstantsPass;
|
||||
use super::forms::{Scheme, encrypt_int, encrypt_long};
|
||||
|
||||
/// Формы обратимы арифметически (интерпретатор троек/пятёрок).
|
||||
/// Forms round-trip arithmetically (an interpreter for triples/quintuples).
|
||||
fn eval_int(insns: &[Insn]) -> i32 {
|
||||
match insns {
|
||||
[
|
||||
|
|
@ -20,7 +20,7 @@ fn eval_int(insns: &[Insn]) -> i32 {
|
|||
match op {
|
||||
Xor => a ^ b,
|
||||
Add => a.wrapping_add(*b),
|
||||
_ => panic!("не та тройка: {insns:?}"),
|
||||
_ => panic!("not a triple: {insns:?}"),
|
||||
}
|
||||
}
|
||||
[
|
||||
|
|
@ -30,7 +30,7 @@ fn eval_int(insns: &[Insn]) -> i32 {
|
|||
Insn::Const(Const::Int(b)),
|
||||
Insn::Binary { .. },
|
||||
] => (a << 1) | (b & 1),
|
||||
_ => panic!("не форма: {insns:?}"),
|
||||
_ => panic!("not a form: {insns:?}"),
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -84,7 +84,7 @@ fn sample() -> Program {
|
|||
}
|
||||
}
|
||||
|
||||
/// Пасс: большие константы разбиты, мелочь и структура целы.
|
||||
/// The pass: big constants are split, trivia and structure survive.
|
||||
#[test]
|
||||
fn constants_split_and_predicated() {
|
||||
let mut program = sample();
|
||||
|
|
@ -96,16 +96,54 @@ fn constants_split_and_predicated() {
|
|||
.apply(&mut ctx, &mut program)
|
||||
.expect("constants");
|
||||
|
||||
let code = program.classes[0].methods[0].code.as_ref().expect("код");
|
||||
let code = program.classes[0].methods[0].code.as_ref().expect("code");
|
||||
let has = |v: i32| code.insns.contains(&Insn::Const(Const::Int(v)));
|
||||
assert!(!has(100), "исходная константа уцелела");
|
||||
assert!(has(3), "мелочь тронута");
|
||||
assert!(!has(100), "source constant survived");
|
||||
assert!(has(3), "trivia touched");
|
||||
assert!(
|
||||
!code.insns.contains(&Insn::Const(Const::Long(10))),
|
||||
"long уцелел"
|
||||
"long survived"
|
||||
);
|
||||
// Opaque-предикат: мёртвая ветвь из `nop`.
|
||||
assert!(code.insns.contains(&Insn::Nop), "нет предиката");
|
||||
assert!(report.transformed >= 3, "мало трансформаций: {report:?}");
|
||||
// Opaque predicate: a dead branch made of `nop`.
|
||||
assert!(code.insns.contains(&Insn::Nop), "no predicate");
|
||||
assert!(report.transformed >= 3, "few transforms: {report:?}");
|
||||
assert_eq!(report.skipped, 0);
|
||||
}
|
||||
|
||||
/// A method that would blow past the 64KB ceiling stays byte-identical.
|
||||
#[test]
|
||||
fn oversized_methods_are_skipped() {
|
||||
let code = Code {
|
||||
max_stack: 2,
|
||||
max_locals: 0,
|
||||
insns: vec![Insn::Const(Const::Int(1_000_000)); MAX_METHOD_INSNS + 1],
|
||||
exception_table: Vec::new(),
|
||||
frames: Vec::new(),
|
||||
attributes: Vec::new(),
|
||||
};
|
||||
let mut program = 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(),
|
||||
};
|
||||
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("code");
|
||||
assert!(
|
||||
code.insns
|
||||
.iter()
|
||||
.all(|i| matches!(i, Insn::Const(Const::Int(1_000_000)))),
|
||||
"oversized method was rewritten"
|
||||
);
|
||||
assert_eq!(report.skipped, 1);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,12 +1,12 @@
|
|||
//! Пасс отладочной информации: удаление и ложная подмена.
|
||||
//!
|
||||
//! Источник требований — `info/evo-weaknesses.md` §3: у разобранного
|
||||
//! Источник требований - `info/evo-weaknesses.md` §3: у разобранного
|
||||
//! образца оригинальные `SourceFile` у 160 классов, 10 218 `LineNumberTable`
|
||||
//! и имена локальных переменных. Это дешевле всего чинить и дороже всего
|
||||
//! отдавать.
|
||||
//!
|
||||
//! Важно: удалять недостаточно. Инструменты (JADX/CFR) верят отладке сильнее,
|
||||
//! чем байткоду, поэтому `spoof_source_file` подставляет **ложное** имя —
|
||||
//! чем байткоду, поэтому `spoof_source_file` подставляет **ложное** имя -
|
||||
//! консистентное, но не связанное с реальным исходником.
|
||||
|
||||
use noxis_classfile::{Attribute, ClassFile, Code, Method, Program};
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@
|
|||
//! Every emitter here is stack-neutral as a whole: whatever the prologue
|
||||
//! pushes it also consumes before falling through to `body`. That is what
|
||||
//! makes prepending safe at method entry (empty stack) without any dataflow
|
||||
//! analysis — all joins at `body` carry an empty stack, so any computed
|
||||
//! analysis - all joins at `body` carry an empty stack, so any computed
|
||||
//! `StackMapTable` stays consistent.
|
||||
|
||||
use noxis_classfile::{Arith, BranchCond, Cond, Const, Insn, Prim, SlotKind};
|
||||
|
|
@ -14,7 +14,7 @@ use noxis_core::Rng;
|
|||
/// The opaque key is computed as `a + b` (two constants, never the key
|
||||
/// itself) and dispatched over `[key-2, key+2]`; only the middle slot reaches
|
||||
/// `body`, the rest fall into a dead block that jumps back to `body`.
|
||||
/// Does not emit `Mark(body)` — the caller places it.
|
||||
/// Does not emit `Mark(body)` - the caller places it.
|
||||
pub(super) fn table_dispatch(body: u32, dead: u32, rng: &mut Rng) -> Vec<Insn> {
|
||||
let key = 100 + (rng.next_u64() % 10_000) as i32;
|
||||
let half = (rng.next_u64() as i32) & 0x3FFF_FFFF;
|
||||
|
|
@ -41,9 +41,9 @@ pub(super) fn table_dispatch(body: u32, dead: u32, rng: &mut Rng) -> Vec<Insn> {
|
|||
/// Sparse `lookupswitch` dispatcher over a counter local (`computed-switch`).
|
||||
///
|
||||
/// The counter lives in `slot` (a fresh local owned by the caller): the shape
|
||||
/// differs from `table_dispatch` on purpose — a sparse switch over a variable
|
||||
/// differs from `table_dispatch` on purpose - a sparse switch over a variable
|
||||
/// instead of a dense range over a stack value. Junk keys never equal `key`.
|
||||
/// Does not emit `Mark(body)` — the caller places it.
|
||||
/// Does not emit `Mark(body)` - the caller places it.
|
||||
pub(super) fn lookup_dispatch(body: u32, dead: u32, slot: u16, rng: &mut Rng) -> Vec<Insn> {
|
||||
let key = 100 + (rng.next_u64() % 10_000) as i32;
|
||||
let mut keys = vec![key];
|
||||
|
|
@ -82,7 +82,7 @@ pub(super) fn lookup_dispatch(body: u32, dead: u32, slot: u16, rng: &mut Rng) ->
|
|||
/// Always-true opaque guard `(C*C + C) & 1 == 0` with a dead `nop` branch.
|
||||
///
|
||||
/// Stack-neutral: the computed value is consumed by the branch that never
|
||||
/// fires. Ends with `Mark(merge)` — execution continues there.
|
||||
/// fires. Ends with `Mark(merge)` - execution continues there.
|
||||
/// Same construction as the constants pass uses; the predicate core is shared
|
||||
/// with the flow splices (`rewire::opaque_compute`).
|
||||
pub(super) fn opaque_guard(dead: u32, merge: u32, rng: &mut Rng) -> Vec<Insn> {
|
||||
|
|
|
|||
|
|
@ -8,7 +8,7 @@
|
|||
//!
|
||||
//! Every scheme also cuts the stream with a few `goto` splices (intra-method
|
||||
//! mixing) and recomputes `StackMapTable` afterwards, so the only thing a
|
||||
//! scheme may assume is an empty stack at method entry — joins stay consistent
|
||||
//! scheme may assume is an empty stack at method entry - joins stay consistent
|
||||
//! by construction (see `dispatch`/`rewire` for why each shape is safe).
|
||||
|
||||
mod dispatch;
|
||||
|
|
@ -120,7 +120,7 @@ fn skippable(name: &str, code: &Code, max_insns: usize) -> bool {
|
|||
if name == "<init>" {
|
||||
return true;
|
||||
}
|
||||
// Frames are never computed for subroutines — keep them byte-identical.
|
||||
// Frames are never computed for subroutines - keep them byte-identical.
|
||||
if code
|
||||
.insns
|
||||
.iter()
|
||||
|
|
@ -167,7 +167,7 @@ fn apply_scheme(code: &mut Code, scheme: Scheme, cfg: &noxis_core::FlowConfig, r
|
|||
prologue.push(Insn::Mark(body));
|
||||
}
|
||||
Scheme::ComputedSwitch if code.max_locals == u16::MAX => {
|
||||
// No free local slot — fall back to the stack-based dispatcher.
|
||||
// No free local slot - fall back to the stack-based dispatcher.
|
||||
prologue.extend(table_dispatch(body, dead, rng));
|
||||
prologue.push(Insn::Mark(body));
|
||||
}
|
||||
|
|
@ -189,7 +189,7 @@ fn apply_scheme(code: &mut Code, scheme: Scheme, cfg: &noxis_core::FlowConfig, r
|
|||
// No skip-goto after the dead block: the dispatcher's dead
|
||||
// `Goto(body)` already jumps over the handler. A second goto
|
||||
// would sit after a terminal with no frame of its own, and
|
||||
// HotSpot requires a frame at every block start — even an
|
||||
// HotSpot requires a frame at every block start - even an
|
||||
// unreachable one. try_e IS the dead mark for the same reason.
|
||||
let (mut handler_code, entry) = dead_catch(try_start, dead, handler, body);
|
||||
guarded.append(&mut handler_code);
|
||||
|
|
|
|||
|
|
@ -77,8 +77,8 @@ pub(super) fn invert_branches(
|
|||
/// Each splice computes an always-false opaque predicate `(C*C + C) & 1 != 0`
|
||||
/// and branches on it: the fallthrough continues the stream, the dead block
|
||||
/// throws instead of rejoining. Throwing (rather than jumping back) keeps the
|
||||
/// splice valid at *any* stack depth — `athrow` has no successor, so there is
|
||||
/// nothing to merge — and the dead block still owns a frame through its entry
|
||||
/// splice valid at *any* stack depth - `athrow` has no successor, so there is
|
||||
/// nothing to merge - and the dead block still owns a frame through its entry
|
||||
/// branch, which HotSpot requires even for code that never executes.
|
||||
///
|
||||
/// Cuts land at or after `start` (the entry prologue must stay first) and
|
||||
|
|
|
|||
|
|
@ -10,7 +10,7 @@ use noxis_core::{Config, Context, NameStyle, Rng};
|
|||
use super::{ACC_PRIVATE, ACC_PROTECTED, ACC_PUBLIC, ACC_STATIC};
|
||||
use crate::namegen::NameGenerator;
|
||||
|
||||
/// `(owner, name, descriptor)` — matches a JVM reference.
|
||||
/// `(owner, name, descriptor)` - matches a JVM reference.
|
||||
pub(super) type MemberKey = (String, String, String);
|
||||
|
||||
/// Rename maps for one run: `declared` is where a member was declared,
|
||||
|
|
|
|||
|
|
@ -4,11 +4,11 @@
|
|||
//! у EvoVis члены не переименовывались вообще, и класс читался по `javap -p`).
|
||||
//!
|
||||
//! Тонкости, которые здесь учитываются:
|
||||
//! - вызов `Sub.staticMethod()` может указывать владельцем подкласс — нужны
|
||||
//! - вызов `Sub.staticMethod()` может указывать владельцем подкласс - нужны
|
||||
//! псевдонимы (`alias`) для всей иерархии;
|
||||
//! - мостовые методы (`ACC_BRIDGE`) и ковариантные возвраты живут в одном
|
||||
//! «семействе» метода — переименовывать их нужно разом;
|
||||
//! - `serialVersionUID`, JNI, рефлексия, сериализация — не трогаем.
|
||||
//! «семействе» метода - переименовывать их нужно разом;
|
||||
//! - `serialVersionUID`, JNI, рефлексия, сериализация - не трогаем.
|
||||
|
||||
/// Карты и стратегия назначения имён.
|
||||
mod assign;
|
||||
|
|
@ -179,7 +179,7 @@ impl Pass for RenamePass {
|
|||
}
|
||||
}
|
||||
|
||||
// 3. Виртуальные семейства — только в агрессивном режиме.
|
||||
// 3. Виртуальные семейства - только в агрессивном режиме.
|
||||
if cfg.rename.rename_public_members {
|
||||
renamed_methods += rename_virtual_families(program, &analysis, &mut maps, &cfg, ctx);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,8 +1,8 @@
|
|||
//! Пасс ресурсов: патч текстовых файлов архива по маппингу классов.
|
||||
//!
|
||||
//! Без этого обфускация Fabric-мода просто не стартует: `fabric.mod.json`
|
||||
//! хранит main-class точкой, `*.mixins.json` — пути внутренними именами,
|
||||
//! refmap и access widener — тоже. Материал: `info/evo-weaknesses.md` §3/§8
|
||||
//! хранит main-class точкой, `*.mixins.json` - пути внутренними именами,
|
||||
//! refmap и access widener - тоже. Материал: `info/evo-weaknesses.md` §3/§8
|
||||
//! (у разобранного образца ресурсы вообще не трогали).
|
||||
//!
|
||||
//! Замена **границная**, а не подстрочная: `org/example/Foo` не должен
|
||||
|
|
@ -33,7 +33,7 @@ const TEXT_EXTS: [&str; 9] = [
|
|||
".accesswidener",
|
||||
];
|
||||
|
||||
/// Service loader'ы: файла без расширения, но каждая строка — имя класса.
|
||||
/// Service loader'ы: файла без расширения, но каждая строка - имя класса.
|
||||
const SERVICES_PREFIX: &str = "META-INF/services/";
|
||||
|
||||
/// Пасс патча ресурсов.
|
||||
|
|
@ -52,7 +52,7 @@ impl Pass for ResourcesPass {
|
|||
pass: PassId::Resources,
|
||||
transformed: 0,
|
||||
skipped: program.resources.len(),
|
||||
notes: vec!["маппинг классов пуст — ресурсы не тронуты".into()],
|
||||
notes: vec!["маппинг классов пуст - ресурсы не тронуты".into()],
|
||||
});
|
||||
}
|
||||
|
||||
|
|
@ -142,7 +142,7 @@ fn replace_delimited(text: &str, old: &str, new: &str, dotted: bool) -> String {
|
|||
continue;
|
||||
}
|
||||
|
||||
// Совпадение частичное — пропускаем первый символ и ищем дальше.
|
||||
// Совпадение частичное - пропускаем первый символ и ищем дальше.
|
||||
let mut chars = rest[at..].chars();
|
||||
out.push_str(before);
|
||||
match chars.next() {
|
||||
|
|
|
|||
|
|
@ -71,7 +71,7 @@ fn empty_map_is_a_noop() {
|
|||
|
||||
#[test]
|
||||
fn unicode_text_survives_unrelated_substitution() {
|
||||
let text = "комментарий: Лорем_ипсум — org/example/Bar";
|
||||
let text = "комментарий: Лорем_ипсум - org/example/Bar";
|
||||
assert_eq!(remap_text(text, &mapping()), text);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
//! Шифрование строк: чанки, salt под схему, последовательность вызова.
|
||||
//!
|
||||
//! Один `Const::String` превращается в `ldc cipher; ldc salt; invokestatic
|
||||
//! d` на чанк; чанки склеиваются `String.concat` (без `StringBuilder` —
|
||||
//! d` на чанк; чанки склеиваются `String.concat` (без `StringBuilder` -
|
||||
//! меньше инструкций и нет вызовов конструктора).
|
||||
|
||||
use noxis_classfile::{Const, Insn, InvokeKind};
|
||||
|
|
@ -24,7 +24,7 @@ pub(super) fn split_chunks(text: &str, split_above: usize) -> Vec<String> {
|
|||
|
||||
/// Salt, попадающий в нужную схему: `salt % total == index`.
|
||||
///
|
||||
/// Salt всегда неотрицательный (Java `%` сохраняет знак — отрицательный salt
|
||||
/// 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;
|
||||
|
|
|
|||
|
|
@ -1,8 +1,8 @@
|
|||
//! Пасс шифрования строк: `Const::String` → вызовы рантайм-декодера.
|
||||
//! The strings pass: `Const::String` becomes runtime-decoder calls.
|
||||
//!
|
||||
//! Схема строки выбирается ротацией (`salt % n`), salt детерминирован
|
||||
//! (KDF или RNG-поток — см. `derive_key`). Декодер генерируется один на
|
||||
//! программу и только если была зашифрована хотя бы одна строка.
|
||||
//! The scheme rotates per string (`salt % n`), the salt is deterministic
|
||||
//! (KDF or RNG stream - see `derive_key`). The decoder is generated once per
|
||||
//! program and only if at least one string was encrypted.
|
||||
|
||||
mod crypt;
|
||||
|
||||
|
|
@ -12,14 +12,14 @@ mod tests;
|
|||
use std::collections::BTreeMap;
|
||||
|
||||
use noxis_analysis::ClassHierarchy;
|
||||
use noxis_classfile::{Const, Insn, Program, compute_frames};
|
||||
use noxis_classfile::{Const, Insn, MAX_METHOD_INSNS, 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};
|
||||
|
||||
/// Пасс шифрования строк.
|
||||
/// The string encryption pass.
|
||||
#[derive(Debug, Default, Clone, Copy)]
|
||||
pub struct StringsPass;
|
||||
|
||||
|
|
@ -35,9 +35,9 @@ impl Pass for StringsPass {
|
|||
schemes.push(Scheme::parse(name).map_err(|e| pass_error(e.to_string()))?);
|
||||
}
|
||||
if schemes.is_empty() {
|
||||
return Err(pass_error("нет схем шифрования"));
|
||||
return Err(pass_error("no encryption schemes"));
|
||||
}
|
||||
// Фаза 1: собрать все шифруемые строки (позиция + текст).
|
||||
// Phase 1: collect every encryptable string (position + text).
|
||||
let mut targets: Vec<(usize, usize, usize, String)> = Vec::new();
|
||||
let mut skipped = 0usize;
|
||||
for (ci, class) in program.classes.iter().enumerate() {
|
||||
|
|
@ -60,10 +60,10 @@ impl Pass for StringsPass {
|
|||
return Ok(report(
|
||||
0,
|
||||
skipped,
|
||||
vec!["нет строк для шифрования".to_string()],
|
||||
vec!["no strings to encrypt".to_string()],
|
||||
));
|
||||
}
|
||||
// Фаза 2: зашифровать и подставить последовательности вызовов.
|
||||
// Phase 2: encrypt and substitute call sequences.
|
||||
let master = ctx.master_seed;
|
||||
let mut rng = ctx.rng("strings");
|
||||
let owner = decoder_name(ctx, program);
|
||||
|
|
@ -83,49 +83,64 @@ impl Pass for StringsPass {
|
|||
replacements.insert((*ci, *mi, *ii), decode_sequence(&owner, &parts));
|
||||
}
|
||||
let mut mutated: Vec<(usize, usize)> = Vec::new();
|
||||
let mut applied = 0usize;
|
||||
let mut per_method: BTreeMap<(usize, usize), usize> = BTreeMap::new();
|
||||
for (ci, mi, _, _) in &targets {
|
||||
*per_method.entry((*ci, *mi)).or_default() += 1;
|
||||
}
|
||||
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 original = std::mem::take(&mut code.insns);
|
||||
let method_targets = per_method.get(&(ci, mi)).copied().unwrap_or(0);
|
||||
let mut fresh = Vec::with_capacity(original.len());
|
||||
let mut touched = false;
|
||||
for (ii, insn) in code.insns.drain(..).enumerate() {
|
||||
for (ii, insn) in original.iter().enumerate() {
|
||||
if let Some(sequence) = replacements.remove(&(ci, mi, ii)) {
|
||||
fresh.extend(sequence);
|
||||
touched = true;
|
||||
} else {
|
||||
fresh.push(insn);
|
||||
fresh.push(insn.clone());
|
||||
}
|
||||
}
|
||||
if fresh.len() > MAX_METHOD_INSNS {
|
||||
// Would risk the 64KB writer ceiling: keep the original
|
||||
// code (and its strings) untouched.
|
||||
code.insns = original;
|
||||
skipped += 1;
|
||||
continue;
|
||||
}
|
||||
code.insns = fresh;
|
||||
if touched {
|
||||
applied += method_targets;
|
||||
code.max_stack = code.max_stack.saturating_add(2);
|
||||
mutated.push((ci, mi));
|
||||
}
|
||||
code.insns = fresh;
|
||||
}
|
||||
}
|
||||
// Фаза 3: класс-декодер один на программу.
|
||||
// Phase 3: one decoder class per program.
|
||||
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: фреймы затронутых методов.
|
||||
ctx.note(format!("string decoder: {owner}"));
|
||||
// Phase 4: frames of the touched methods.
|
||||
let hierarchy = ClassHierarchy::build(program);
|
||||
for (ci, mi) in &mutated {
|
||||
let (name, snapshot) = {
|
||||
let class = program
|
||||
.classes
|
||||
.get(*ci)
|
||||
.ok_or_else(|| pass_error("класс пропал"))?;
|
||||
.ok_or_else(|| pass_error("class vanished"))?;
|
||||
(
|
||||
class.this_class.clone(),
|
||||
class
|
||||
.methods
|
||||
.get(*mi)
|
||||
.cloned()
|
||||
.ok_or_else(|| pass_error("метод пропал"))?,
|
||||
.ok_or_else(|| pass_error("method vanished"))?,
|
||||
)
|
||||
};
|
||||
let frames = compute_frames(&name, &snapshot, &hierarchy)
|
||||
|
|
@ -135,16 +150,13 @@ impl Pass for StringsPass {
|
|||
.get_mut(*ci)
|
||||
.and_then(|c| c.methods.get_mut(*mi))
|
||||
.and_then(|m| m.code.as_mut())
|
||||
.ok_or_else(|| pass_error("метод пропал"))?
|
||||
.ok_or_else(|| pass_error("method vanished"))?
|
||||
.frames = frames;
|
||||
}
|
||||
Ok(report(
|
||||
targets.len(),
|
||||
applied,
|
||||
skipped,
|
||||
vec![format!(
|
||||
"зашифровано строк: {}, декодер: {owner}",
|
||||
targets.len()
|
||||
)],
|
||||
vec![format!("strings encrypted: {applied}, decoder: {owner}")],
|
||||
))
|
||||
}
|
||||
}
|
||||
|
|
@ -162,12 +174,12 @@ fn pass_error(msg: impl Into<String>) -> CoreError {
|
|||
CoreError::pass(PassId::Strings, msg)
|
||||
}
|
||||
|
||||
/// Строка подпадает под `skip`-паттерны конфига.
|
||||
/// A string matches the `skip` patterns of the config.
|
||||
fn is_skipped(text: &str, skip: &[String]) -> bool {
|
||||
skip.iter().any(|pattern| text.contains(pattern))
|
||||
}
|
||||
|
||||
/// Salt чанка: KDF от позиции или RNG-поток (см. `derive_key`).
|
||||
/// A chunk salt: KDF from the position or an RNG stream (see `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)
|
||||
|
|
@ -178,7 +190,7 @@ fn next_salt(rng: &mut noxis_core::Rng, master: u64, counter: &mut u64, derive:
|
|||
value
|
||||
}
|
||||
|
||||
/// Детерминированное имя декодера без коллизий с классами программы.
|
||||
/// A collision-free deterministic decoder name.
|
||||
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 {
|
||||
|
|
|
|||
|
|
@ -1,8 +1,8 @@
|
|||
//! Тесты пасса строк: подстановка, чанки, детерминизм шифротекста.
|
||||
//! String-pass tests: substitution, chunks, ciphertext determinism.
|
||||
|
||||
#![allow(clippy::expect_used, clippy::unwrap_used)]
|
||||
|
||||
use noxis_classfile::{ClassBuilder, Code, Const, Insn, Program, Version};
|
||||
use noxis_classfile::{ClassBuilder, Code, Const, Insn, MAX_METHOD_INSNS, Program, Version};
|
||||
use noxis_core::{Config, Context};
|
||||
use noxis_runtime::{Scheme, encrypt};
|
||||
|
||||
|
|
@ -36,7 +36,7 @@ fn sample() -> Program {
|
|||
}
|
||||
}
|
||||
|
||||
/// Короткие строки не режутся, длинные — на границе символов.
|
||||
/// Short strings stay whole, long ones split on char boundaries.
|
||||
#[test]
|
||||
fn chunks_split_by_chars() {
|
||||
assert_eq!(split_chunks("abc", 64), vec!["abc".to_string()]);
|
||||
|
|
@ -45,19 +45,19 @@ fn chunks_split_by_chars() {
|
|||
assert_eq!(chunks.concat().len(), 100);
|
||||
}
|
||||
|
||||
/// Salt всегда неотрицателен и попадает в схему.
|
||||
/// Salt is always non-negative and lands in its scheme.
|
||||
#[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!(salt >= 0, "negative salt");
|
||||
assert_eq!(salt as usize % total, index);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Пасс: строки заменены вызовами, шифротекст воспроизводим, декодер есть.
|
||||
/// The pass: strings become calls, ciphertext is reproducible, decoder exists.
|
||||
#[test]
|
||||
fn strings_replaced_and_deterministic() {
|
||||
let mut program = sample();
|
||||
|
|
@ -70,14 +70,14 @@ fn strings_replaced_and_deterministic() {
|
|||
|
||||
assert_eq!(report.transformed, 2);
|
||||
assert_eq!(report.skipped, 1);
|
||||
// Декодер добавлен последним.
|
||||
// The decoder is appended last.
|
||||
assert_eq!(program.classes.len(), 2);
|
||||
let decoder = program.classes.last().expect("декодер");
|
||||
let decoder = program.classes.last().expect("decoder");
|
||||
assert!(decoder.methods.iter().any(|m| m.name == "d"));
|
||||
|
||||
// Исходных строк не осталось (шифры — длинные Const::String — на месте);
|
||||
// короткая на месте.
|
||||
let code = program.classes[0].methods[0].code.as_ref().expect("код");
|
||||
// No source strings left (ciphers - long Const::String - are in place);
|
||||
// the short one stays.
|
||||
let code = program.classes[0].methods[0].code.as_ref().expect("code");
|
||||
let kept: Vec<&String> = code
|
||||
.insns
|
||||
.iter()
|
||||
|
|
@ -90,7 +90,7 @@ fn strings_replaced_and_deterministic() {
|
|||
assert!(!kept.iter().any(|s| *s == &"y".repeat(100)));
|
||||
assert!(kept.contains(&&"x".to_string()));
|
||||
|
||||
// Каждый вызов сверяется с прямым шифрованием (тот же сид → те же блобы).
|
||||
// Every call checks out against direct encryption (same seed, same blobs).
|
||||
let schemes = [Scheme::XorSplitmix, Scheme::XteaCtr];
|
||||
let mut triples = Vec::new();
|
||||
let mut window: Vec<&Insn> = Vec::new();
|
||||
|
|
@ -109,7 +109,7 @@ fn strings_replaced_and_deterministic() {
|
|||
triples.push((cipher.clone(), *salt));
|
||||
}
|
||||
}
|
||||
// "hello world" (1 чанк) + "y"*100 (2 чанка).
|
||||
// "hello world" (1 chunk) + "y"*100 (2 chunks).
|
||||
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) {
|
||||
|
|
@ -117,7 +117,7 @@ fn strings_replaced_and_deterministic() {
|
|||
let blob = encrypt(scheme, 7, *salt, &original);
|
||||
assert_eq!(cipher, &blob.cipher);
|
||||
}
|
||||
// Склейка чанков через `concat` — ровно одна; конструктор жив.
|
||||
// Chunk joins through `concat` - exactly one; the constructor is alive.
|
||||
assert_eq!(
|
||||
code.insns
|
||||
.iter()
|
||||
|
|
@ -127,3 +127,39 @@ fn strings_replaced_and_deterministic() {
|
|||
);
|
||||
assert!(!decode_sequence("o", &[("c".to_string(), 1)]).is_empty());
|
||||
}
|
||||
|
||||
/// A method that would blow past the 64KB ceiling keeps its strings.
|
||||
#[test]
|
||||
fn oversized_methods_are_skipped() {
|
||||
let code = Code {
|
||||
max_stack: 2,
|
||||
max_locals: 0,
|
||||
insns: vec![Insn::Const(Const::String("oversized".to_string())); MAX_METHOD_INSNS + 1],
|
||||
exception_table: Vec::new(),
|
||||
frames: Vec::new(),
|
||||
attributes: Vec::new(),
|
||||
};
|
||||
let mut program = 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(),
|
||||
};
|
||||
let mut ctx = Context::new(Config {
|
||||
seed: Some(7),
|
||||
..Config::default()
|
||||
});
|
||||
let report = StringsPass.apply(&mut ctx, &mut program).expect("strings");
|
||||
|
||||
let code = program.classes[0].methods[0].code.as_ref().expect("code");
|
||||
assert!(
|
||||
code.insns
|
||||
.iter()
|
||||
.all(|i| matches!(i, Insn::Const(Const::String(s)) if s == "oversized")),
|
||||
"oversized method was rewritten"
|
||||
);
|
||||
assert_eq!(report.skipped, 1);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@
|
|||
//! Вход собирается `ByteBuffer`, чтобы не материализовывать 26 байт
|
||||
//! поштучно: `allocate.putLong(master).put(domain).putLong(salt).array()`.
|
||||
//!
|
||||
//! Здесь же — строитель тела метода и короткие эмиссии (общие для всех
|
||||
//! Здесь же - строитель тела метода и короткие эмиссии (общие для всех
|
||||
//! ветвей декодера).
|
||||
|
||||
use noxis_classfile::{Arith, BranchCond, Cond, Const, Insn, InvokeKind, Label, Prim, SlotKind};
|
||||
|
|
@ -21,7 +21,7 @@ impl Gen {
|
|||
Self {
|
||||
insns: Vec::new(),
|
||||
labels: 0,
|
||||
// Слоты 0 (`cipher`) и 1 (`salt`) — параметры метода.
|
||||
// Слоты 0 (`cipher`) и 1 (`salt`) - параметры метода.
|
||||
slots: 2,
|
||||
}
|
||||
}
|
||||
|
|
@ -32,7 +32,7 @@ impl Gen {
|
|||
label
|
||||
}
|
||||
|
||||
/// Занять слоты (`wide` — пара под `long`).
|
||||
/// Занять слоты (`wide` - пара под `long`).
|
||||
pub(super) fn alloc(&mut self, wide: bool) -> u16 {
|
||||
let slot = self.slots;
|
||||
self.slots += if wide { 2 } else { 1 };
|
||||
|
|
@ -134,7 +134,7 @@ pub(super) fn emit_kdf(asm: &mut Gen, master: u16, domain: &str) -> u16 {
|
|||
|
||||
/// Собрать вход `master || domain || salt` в слот `input`.
|
||||
///
|
||||
/// Rust считает FNV по little-endian байтам, а `putLong` кладёт big-endian —
|
||||
/// 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));
|
||||
|
|
|
|||
|
|
@ -6,8 +6,8 @@
|
|||
//! static String d(String cipher, int salt)
|
||||
//! ```
|
||||
//!
|
||||
//! - `cipher` — base64 шифротекста (схема выбирается по salt: `scheme = all[salt % n]`);
|
||||
//! - `salt` — индекс строки, по нему детерминированно выводятся key и nonce;
|
||||
//! - `cipher` - base64 шифротекста (схема выбирается по salt: `scheme = all[salt % n]`);
|
||||
//! - `salt` - индекс строки, по нему детерминированно выводятся key и nonce;
|
||||
//! - мастер-секрет **не передаётся на вызове** и не лежит в одной константе:
|
||||
//! он собирается из двух частей (`k1 ^ k2`) прямо в декодере.
|
||||
//!
|
||||
|
|
@ -63,7 +63,7 @@ pub fn decoder_class(
|
|||
emit_dispatch(&mut asm, &parsed, key, nonce, data, len);
|
||||
emit_epilogue(&mut asm, data);
|
||||
let code = Code {
|
||||
// Точный пик — 10 слотов (замер 08.10.2026); запас без вреда:
|
||||
// Точный пик - 10 слотов (замер 08.10.2026); запас без вреда:
|
||||
// лимит обязан лишь покрывать фактическую глубину.
|
||||
max_stack: 12,
|
||||
max_locals: asm.slots,
|
||||
|
|
@ -88,7 +88,7 @@ pub fn decoder_owner(seed_name: &str) -> String {
|
|||
}
|
||||
|
||||
/// Пролог: `master = k1 ^ k2`; возвращает слот пары. Ни одна часть не равна
|
||||
/// мастер-секрету (вторая — `master ^ k1`, первая — потоковая).
|
||||
/// мастер-секрету (вторая - `master ^ k1`, первая - потоковая).
|
||||
fn emit_master(asm: &mut Gen, master: u64) -> u16 {
|
||||
use noxis_core::Rng;
|
||||
|
||||
|
|
@ -135,7 +135,7 @@ fn emit_len(asm: &mut Gen, data: u16) -> u16 {
|
|||
slot
|
||||
}
|
||||
|
||||
/// Диспетчер схем: `salt % n` лестницей `if_icmpne` (n == 1 — без диспетчера).
|
||||
/// Диспетчер схем: `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);
|
||||
|
|
|
|||
|
|
@ -7,7 +7,7 @@ use noxis_classfile::{Arith, ArrayKind, BranchCond, Cond, Convert, Insn, Prim, S
|
|||
|
||||
use super::kdf::{Gen, c_int, c_long, load, store};
|
||||
|
||||
/// Расшифровать `data` на месте; `len` — длина массива.
|
||||
/// Расшифровать `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);
|
||||
|
|
@ -76,7 +76,7 @@ fn emit_states(asm: &mut Gen, slots: &Slots) {
|
|||
}
|
||||
}
|
||||
|
||||
/// Один `splitmix64`: `sm += PHI`, финализатор — в `out`.
|
||||
/// Один `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;
|
||||
|
|
@ -105,7 +105,7 @@ fn finalize(asm: &mut Gen, amount: i32, constant: i64) {
|
|||
asm.emit(long_op(Arith::Mul));
|
||||
}
|
||||
|
||||
/// Цикл по чанкам: свежий `next_u64` на 8 байт, внутренний — по байтам.
|
||||
/// Цикл по чанкам: свежий `next_u64` на 8 байт, внутренний - по байтам.
|
||||
///
|
||||
/// Без `skip`-джойна: стрим всегда свеж на входе во внутренний цикл.
|
||||
fn emit_loop(asm: &mut Gen, data: u16, len: u16, slots: &Slots) {
|
||||
|
|
@ -188,7 +188,7 @@ fn emit_next(asm: &mut Gen, slots: &Slots) {
|
|||
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 — строго по порядку.
|
||||
// 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]);
|
||||
|
|
|
|||
|
|
@ -1,16 +1,16 @@
|
|||
//! Ветвь `xtea-ctr`: XTEA в режиме счётчика, побайтовый XOR на месте.
|
||||
//!
|
||||
//! Зеркало `scheme::xtea_ctr` побитово: ключ — четыре `u32` из key/nonce,
|
||||
//! счётчик — номер чанка, все правые сдвиги — логические (`u32` в Rust).
|
||||
//! Зеркало `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`.
|
||||
/// XTEA delta - знаковое представление для `ldc_w`.
|
||||
const DELTA: i32 = 0x9E37_79B9u32 as i32;
|
||||
|
||||
/// Расшифровать `data` на месте; `len` — длина массива.
|
||||
/// Расшифровать `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);
|
||||
|
|
@ -168,7 +168,7 @@ fn mixing(asm: &mut Gen, dst: u16, src: u16, key_shift: i32, slots: &Slots) {
|
|||
asm.emit(store(dst, SlotKind::Int));
|
||||
}
|
||||
|
||||
/// XOR 8 байт чанка со стримом `(v0, v1)`; хвост короче 8 — частично.
|
||||
/// 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));
|
||||
|
|
|
|||
|
|
@ -1,11 +1,11 @@
|
|||
//! Схемы шифрования строк: пер-строчный ключ, nonce и несколько алгоритмов.
|
||||
//!
|
||||
//! Требования (см. `info/evo-weaknesses.md` и `docs/techniques.md`):
|
||||
//! - **разные схемы между строками** — один скрипт не покрывает весь jar;
|
||||
//! - **ключ не рядом с шифротекстом** — на вызов передаётся только salt,
|
||||
//! мастер-секрет живёт в рантайм-декодере (сборка мастер-секрета — P2);
|
||||
//! - **nonce уникален на строку** — keystream не повторяется;
|
||||
//! - **MAC** — повреждение/подмена шифротекста ловится, а не молча портит текст.
|
||||
//! - **разные схемы между строками** - один скрипт не покрывает весь jar;
|
||||
//! - **ключ не рядом с шифротекстом** - на вызов передаётся только salt,
|
||||
//! мастер-секрет живёт в рантайм-декодере (сборка мастер-секрета - P2);
|
||||
//! - **nonce уникален на строку** - keystream не повторяется;
|
||||
//! - **MAC** - повреждение/подмена шифротекста ловится, а не молча портит текст.
|
||||
|
||||
use noxis_core::{Rng, derive_seed};
|
||||
|
||||
|
|
@ -58,7 +58,7 @@ impl Scheme {
|
|||
pub struct Blob {
|
||||
/// base64 шифротекста.
|
||||
pub cipher: String,
|
||||
/// 32-битный MAC по ключу от plaintext — ловит подмену/порчу.
|
||||
/// 32-битный MAC по ключу от plaintext - ловит подмену/порчу.
|
||||
pub tag: u32,
|
||||
}
|
||||
|
||||
|
|
@ -67,7 +67,7 @@ pub fn string_key(master: u64, salt: i32) -> u64 {
|
|||
derive_seed(master, "string/key", u64::from(salt as u32))
|
||||
}
|
||||
|
||||
/// Nonce строки: детерминированный от salt, но другой домен — keystream не
|
||||
/// Nonce строки: детерминированный от salt, но другой домен - keystream не
|
||||
/// пересекается даже при совпадении ключей.
|
||||
pub fn string_nonce(master: u64, salt: i32) -> u64 {
|
||||
derive_seed(master, "string/nonce", u64::from(salt as u32))
|
||||
|
|
@ -123,7 +123,7 @@ fn xor_splitmix(key: u64, nonce: u64, data: &mut [u8]) {
|
|||
}
|
||||
}
|
||||
|
||||
/// XTEA в режиме счётчика: keystream = XTEA(k, counter), XOR — на месте.
|
||||
/// XTEA в режиме счётчика: keystream = XTEA(k, counter), XOR - на месте.
|
||||
fn xtea_ctr(key: u64, nonce: u64, data: &mut [u8]) {
|
||||
let k = [
|
||||
key as u32,
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
//! Сквозная проверка декодера: шифруем в Rust, расшифровываем настоящим `java`.
|
||||
//!
|
||||
//! Собираются два класса (декодер + вызывающий `main`), пишутся на диск,
|
||||
//! `java` исполняет — stdout обязан совпасть с исходными строками. Без `java`
|
||||
//! `java` исполняет - stdout обязан совпасть с исходными строками. Без `java`
|
||||
//! тест пропускается (graceful skip, как в `noxis-classfile/tests/frames.rs`).
|
||||
|
||||
#![allow(clippy::expect_used, clippy::unwrap_used)]
|
||||
|
|
|
|||
|
|
@ -1,10 +1,10 @@
|
|||
//! Project structure rules, invoked from every crate's `build.rs`.
|
||||
//!
|
||||
//! Rules apply to Rust code only, not to documentation:
|
||||
//! * every `.rs` file holds at most [`MAX_FILE_LINES`] *code* lines — blank
|
||||
//! * every `.rs` file holds at most [`MAX_FILE_LINES`] *code* lines - blank
|
||||
//! lines and full-line comments (`//`, `///`, `//!`) are not counted;
|
||||
//! * every directory inside a crate's `src/`, `tests/` or `benches/` holds
|
||||
//! at most [`MAX_DIR_FILES`] files — subdirectories are unlimited.
|
||||
//! at most [`MAX_DIR_FILES`] files - subdirectories are unlimited.
|
||||
//!
|
||||
//! Deliberately excluded: `Cargo.lock`, `.gitignore`, `docs/`, the legacy
|
||||
//! `info/` tree, and crate roots (they always own `Cargo.toml` plus
|
||||
|
|
@ -34,13 +34,13 @@ const SKIPPED_DIRS: [&str; 2] = ["target", ".git"];
|
|||
///
|
||||
/// # Panics
|
||||
///
|
||||
/// Panics whenever a rule is violated — that is the point of the gate.
|
||||
/// Panics whenever a rule is violated - that is the point of the gate.
|
||||
pub fn check() {
|
||||
let manifest = manifest_dir();
|
||||
let mut violations = Vec::new();
|
||||
|
||||
// `src/` is required; `tests/` and `benches/` are checked only when they
|
||||
// exist — otherwise the line rule is bypassed by moving code into tests.
|
||||
// exist - otherwise the line rule is bypassed by moving code into tests.
|
||||
// `docs/` stays out of the gate: the rules are about Rust code only.
|
||||
let roots = [
|
||||
manifest.join("src"),
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@
|
|||
//!
|
||||
//! Каждый класс грузится отдельной JVM в дереве пакетов: изоляция важнее
|
||||
//! скорости (один битый класс не тянет за собой остальные). Классы, которые
|
||||
//! не грузятся из-за отсутствующих зависимостей, — `Skipped`, а не провал:
|
||||
//! не грузятся из-за отсутствующих зависимостей, - `Skipped`, а не провал:
|
||||
//! судить о них верификатор не может.
|
||||
|
||||
use std::path::Path;
|
||||
|
|
@ -13,7 +13,7 @@ use std::process::Command;
|
|||
pub enum JavaVerdict {
|
||||
/// Загрузился без ошибок верификации (`main` при этом может отсутствовать).
|
||||
Verified,
|
||||
/// Верификатор отверг класс (текст ошибки — для отчёта).
|
||||
/// Верификатор отверг класс (текст ошибки - для отчёта).
|
||||
Failed(String),
|
||||
/// Судить нельзя: нет `java` или не хватает зависимостей.
|
||||
Skipped(String),
|
||||
|
|
@ -51,7 +51,7 @@ pub fn verify_class_bytes(internal_name: &str, bytes: &[u8]) -> JavaVerdict {
|
|||
verdict
|
||||
}
|
||||
|
||||
/// Проверить все классы jar'а (фильтр — префиксы внутренних имён).
|
||||
/// Проверить все классы jar'а (фильтр - префиксы внутренних имён).
|
||||
pub fn verify_jar(jar: &Path, scope: &[String]) -> Vec<(String, JavaVerdict)> {
|
||||
let entries = match noxis_jar::read_file(jar) {
|
||||
Ok(entries) => entries,
|
||||
|
|
|
|||
|
|
@ -24,7 +24,7 @@ pub const RED_PATTERNS: &[&str] = &[
|
|||
|
||||
/// Все идентификаторы программы: классы, члены, строки кода.
|
||||
///
|
||||
/// Члены — по простому имени (без владельца): сверка строгая и дешёвая,
|
||||
/// Члены - по простому имени (без владельца): сверка строгая и дешёвая,
|
||||
/// для точечной проверки есть `find_leaks` с keep-фильтром.
|
||||
pub fn collect_names(program: &Program) -> BTreeSet<String> {
|
||||
let mut names = BTreeSet::new();
|
||||
|
|
@ -42,7 +42,7 @@ pub fn collect_names(program: &Program) -> BTreeSet<String> {
|
|||
|
||||
/// Имена из эталона, пережившие обфускацию (минус keep).
|
||||
///
|
||||
/// Пустой результат — норма; непустой — тест падает с этим списком.
|
||||
/// Пустой результат - норма; непустой - тест падает с этим списком.
|
||||
pub fn find_leaks(before: &BTreeSet<String>, after: &Program, keep: &KeepRules) -> Vec<String> {
|
||||
let names = collect_names(after);
|
||||
before
|
||||
|
|
|
|||
|
|
@ -2,9 +2,9 @@
|
|||
//!
|
||||
//! Проверка результата обфускации, а не процесса:
|
||||
//!
|
||||
//! * [`leaks`] — утечки исходных имён + секреты открытым текстом;
|
||||
//! * [`java`] — настоящий `java -Xverify:all` по байтам на диске;
|
||||
//! * [`metrics`] — счётчики программы, раздувание, стабильность маппингов.
|
||||
//! * [`leaks`] - утечки исходных имён + секреты открытым текстом;
|
||||
//! * [`java`] - настоящий `java -Xverify:all` по байтам на диске;
|
||||
//! * [`metrics`] - счётчики программы, раздувание, стабильность маппингов.
|
||||
|
||||
pub mod java;
|
||||
pub mod leaks;
|
||||
|
|
|
|||
|
|
@ -1,123 +0,0 @@
|
|||
//! Одноразовый отладочный обход байткода (scratch): ручной парсинг пула и
|
||||
//! Code-атрибутов, вывод заголовков вложенных атрибутов.
|
||||
#![allow(clippy::unwrap_used, clippy::expect_used)]
|
||||
|
||||
fn rd_u16(b: &[u8], p: &mut usize) -> u16 {
|
||||
let v = u16::from_be_bytes([b[*p], b[*p + 1]]);
|
||||
*p += 2;
|
||||
v
|
||||
}
|
||||
fn rd_u32(b: &[u8], p: &mut usize) -> u32 {
|
||||
let v = u32::from_be_bytes([b[*p], b[*p + 1], b[*p + 2], b[*p + 3]]);
|
||||
*p += 4;
|
||||
v
|
||||
}
|
||||
fn hex(b: &[u8]) -> String {
|
||||
b.iter()
|
||||
.map(|x| format!("{x:02x}"))
|
||||
.collect::<Vec<_>>()
|
||||
.join(" ")
|
||||
}
|
||||
fn main() {
|
||||
let entries = noxis_jar::read_file(std::path::Path::new(
|
||||
"/storage/project/jvm/LoReport/build/libs/lorep-1.1.0.jar",
|
||||
))
|
||||
.unwrap();
|
||||
let program = noxis_jar::load(entries).unwrap();
|
||||
let class = program
|
||||
.classes
|
||||
.iter()
|
||||
.find(|c| c.this_class.ends_with("ReportStatsCommand"))
|
||||
.unwrap();
|
||||
let prog = noxis_classfile::Program {
|
||||
classes: vec![class.clone()],
|
||||
resources: vec![],
|
||||
};
|
||||
let out = noxis_jar::save(&prog).unwrap();
|
||||
let entry = out.iter().find(|e| e.name.ends_with(".class")).unwrap();
|
||||
let bytes = &entry.data;
|
||||
// find method_execute Code attr -> sub attrs, dump each sub-attr name_idx + len + first bytes
|
||||
// manual walk: pool
|
||||
let mut pos = 8;
|
||||
let pool_count = rd_u16(bytes, &mut pos);
|
||||
// collect utf8 entries for name resolution
|
||||
let mut utf8s: std::collections::BTreeMap<u16, String> = Default::default();
|
||||
let mut idx = 1u16;
|
||||
while idx < pool_count {
|
||||
let tag = bytes[pos];
|
||||
pos += 1;
|
||||
match tag {
|
||||
1 => {
|
||||
let len = rd_u16(bytes, &mut pos) as usize;
|
||||
utf8s.insert(
|
||||
idx,
|
||||
String::from_utf8_lossy(&bytes[pos..pos + len]).into_owned(),
|
||||
);
|
||||
pos += len;
|
||||
}
|
||||
3 | 4 => pos += 4,
|
||||
5 | 6 => {
|
||||
pos += 8;
|
||||
idx += 1;
|
||||
}
|
||||
7 | 8 | 16 | 19 | 20 => pos += 2,
|
||||
9 | 10 | 11 | 12 | 17 | 18 => pos += 4,
|
||||
15 => pos += 3,
|
||||
_ => {
|
||||
eprintln!("pool tag {tag}");
|
||||
return;
|
||||
}
|
||||
}
|
||||
idx += 1;
|
||||
}
|
||||
let name_of = |i: u16| utf8s.get(&i).cloned().unwrap_or(format!("#{i}"));
|
||||
pos += 6;
|
||||
let ifaces = rd_u16(bytes, &mut pos);
|
||||
pos += 2 * ifaces as usize;
|
||||
let fields = rd_u16(bytes, &mut pos);
|
||||
for _ in 0..fields {
|
||||
pos += 6;
|
||||
let n = rd_u16(bytes, &mut pos);
|
||||
for _ in 0..n {
|
||||
pos += 2;
|
||||
let l = rd_u32(bytes, &mut pos);
|
||||
pos += l as usize;
|
||||
}
|
||||
}
|
||||
let methods = rd_u16(bytes, &mut pos);
|
||||
for _ in 0..methods {
|
||||
pos += 2;
|
||||
let ni = rd_u16(bytes, &mut pos);
|
||||
pos += 2;
|
||||
let an = rd_u16(bytes, &mut pos);
|
||||
let mname = name_of(ni);
|
||||
for _ in 0..an {
|
||||
let aname = rd_u16(bytes, &mut pos);
|
||||
let alen = rd_u32(bytes, &mut pos) as usize;
|
||||
let aname_s = name_of(aname);
|
||||
if aname_s == "Code" {
|
||||
let start = pos;
|
||||
pos += 4;
|
||||
let clen = rd_u32(bytes, &mut pos) as usize;
|
||||
pos += clen;
|
||||
let exc = rd_u16(bytes, &mut pos) as usize;
|
||||
pos += 8 * exc;
|
||||
let subn = rd_u16(bytes, &mut pos);
|
||||
for _ in 0..subn {
|
||||
let sname = rd_u16(bytes, &mut pos);
|
||||
let slen = rd_u32(bytes, &mut pos) as usize;
|
||||
eprintln!(
|
||||
"{mname}: sub={} len={slen} head=[{}]",
|
||||
name_of(sname),
|
||||
hex(&bytes[pos..pos + slen.min(24)])
|
||||
);
|
||||
pos += slen;
|
||||
}
|
||||
assert_eq!(pos, start + alen, "Code length mismatch in {mname}");
|
||||
} else {
|
||||
pos += alen;
|
||||
}
|
||||
}
|
||||
}
|
||||
eprintln!("walk ok");
|
||||
}
|
||||
|
|
@ -11,7 +11,7 @@ use noxis_core::Config;
|
|||
pub struct Args {
|
||||
/// Входной jar.
|
||||
pub input: PathBuf,
|
||||
/// TOML-конфиг с keep-паттернами (без него — пустой keep).
|
||||
/// TOML-конфиг с keep-паттернами (без него - пустой keep).
|
||||
#[arg(short, long)]
|
||||
pub config: Option<PathBuf>,
|
||||
}
|
||||
|
|
|
|||
|
|
@ -13,10 +13,10 @@ pub struct Args {
|
|||
/// Output jar.
|
||||
#[arg(short, long)]
|
||||
pub output: PathBuf,
|
||||
/// TOML config (without it — the Standard profile).
|
||||
/// TOML config (without it - the Standard profile).
|
||||
#[arg(short, long)]
|
||||
pub config: Option<PathBuf>,
|
||||
/// Master seed (deterministic by default — taken from the config).
|
||||
/// Master seed (deterministic by default - taken from the config).
|
||||
#[arg(long)]
|
||||
pub seed: Option<u64>,
|
||||
/// Where to write the JSON report (next to the output by default).
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
//! Библиотека CLI: команды как вызываемые функции (тонкий `main` — в `main.rs`).
|
||||
//! Библиотека CLI: команды как вызываемые функции (тонкий `main` - в `main.rs`).
|
||||
|
||||
pub mod cmd;
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue