passes: emit strings as CONSTANT_Dynamic (condy) - BSM-resolved, invisible to concrete-execution deobfuscators

This commit is contained in:
Loki 2026-10-15 13:57:19 +02:00
parent 1ddc4afbfc
commit aac7571bbc
Signed by: boba
GPG key ID: 253067914055423B
10 changed files with 517 additions and 5 deletions

View file

@ -69,7 +69,14 @@ pub(super) fn build_rename(t: &Table) -> Res<RenameConfig> {
pub(super) fn build_strings(t: &Table) -> Res<StringsConfig> { pub(super) fn build_strings(t: &Table) -> Res<StringsConfig> {
check_known_keys( check_known_keys(
t, t,
&["schemes", "min_len", "split_above", "skip", "derive_key"], &[
"schemes",
"min_len",
"split_above",
"skip",
"derive_key",
"condy",
],
"strings", "strings",
)?; )?;
let mut c = StringsConfig::default(); let mut c = StringsConfig::default();
@ -88,6 +95,9 @@ pub(super) fn build_strings(t: &Table) -> Res<StringsConfig> {
if let Some(v) = t.get("derive_key") { if let Some(v) = t.get("derive_key") {
c.derive_key = expect_bool(v, "strings.derive_key")?; c.derive_key = expect_bool(v, "strings.derive_key")?;
} }
if let Some(v) = t.get("condy") {
c.condy = expect_bool(v, "strings.condy")?;
}
Ok(c) Ok(c)
} }

View file

@ -62,3 +62,10 @@ fn parses_name_style_in_rename_section() {
let cfg = build_from("[rename]\nclass_style = \"il-o0\"\n").unwrap(); let cfg = build_from("[rename]\nclass_style = \"il-o0\"\n").unwrap();
assert_eq!(cfg.rename.class_style, NameStyle::IlO0); assert_eq!(cfg.rename.class_style, NameStyle::IlO0);
} }
#[test]
fn parses_condy_flag_in_strings_section() {
let cfg = build_from("[strings]\ncondy = true\n").unwrap();
assert!(cfg.strings.condy);
assert!(!Config::default().strings.condy);
}

View file

@ -79,6 +79,12 @@ pub struct StringsConfig {
/// Derive the key from KDF(class, method, index) instead of placing it /// Derive the key from KDF(class, method, index) instead of placing it
/// next to the ciphertext. /// next to the ciphertext.
pub derive_key: bool, pub derive_key: bool,
/// Emit strings as `CONSTANT_Dynamic` (condy) resolved by the decoder's
/// bootstrap method instead of direct `d(cipher, salt)` calls. A
/// concrete-execution deobfuscator cannot follow the condy resolution
/// (todo.md, 2026-10-14: SSVM made 0 changes on a condy-only class).
/// Requires classfile >= 55; older classes keep direct calls.
pub condy: bool,
} }
impl Default for StringsConfig { impl Default for StringsConfig {
@ -89,6 +95,7 @@ impl Default for StringsConfig {
split_above: 64, split_above: 64,
skip: Vec::new(), skip: Vec::new(),
derive_key: true, derive_key: true,
condy: false,
} }
} }
} }

View file

@ -0,0 +1,76 @@
//! Condensed emission (`CONSTANT_Dynamic`): the JVM resolves the string
//! itself through the decoder's bootstrap method instead of us calling `d`.
//!
//! Why: a concrete-execution deobfuscator (narumii/SSVM, todo.md 2026-10-14)
//! follows `invokestatic d` and recovers the plaintext, but made **0
//! changes** on a condy-only class - it does not resolve `CONSTANT_Dynamic`
//! at all. The plaintext then lives only inside JVM constant caching.
use noxis_classfile::{Attribute, BootstrapMethod, ClassFile, Const, Insn};
use noxis_runtime::{BOOTSTRAP_DESC, BOOTSTRAP_METHOD};
/// `REF_invokeStatic` (JVMS §5.4.3.5).
const INVOKE_STATIC: u8 = 6;
/// Class-file version introducing `CONSTANT_Dynamic` (Java 11).
pub(super) const CONDY_MIN_MAJOR: u16 = 55;
/// The class supports condy constants at all.
pub(super) fn supported(class: &ClassFile) -> bool {
class.version.major >= CONDY_MIN_MAJOR
}
/// Where new bootstrap entries would start: right after the existing ones
/// (Kotlin lambdas etc. already own the front of the attribute).
pub(super) fn base_index(class: &ClassFile) -> u16 {
class
.attributes
.iter()
.find_map(|attribute| match attribute {
Attribute::BootstrapMethods(list) => Some(list.len() as u16),
_ => None,
})
.unwrap_or(0)
}
/// One bootstrap entry per chunk: the decoder's `b` with `cipher` + `salt`
/// as static arguments. The BSM prefix (`lookup`, `name`, `type`) is filled
/// in by the JVM (JVMS §5.4.3.5).
pub(super) fn entry(owner: &str, cipher: &str, salt: i32) -> BootstrapMethod {
BootstrapMethod {
kind: INVOKE_STATIC,
owner: owner.to_string(),
name: BOOTSTRAP_METHOD.to_string(),
descriptor: BOOTSTRAP_DESC.to_string(),
itf: false,
args: vec![Const::String(cipher.to_string()), Const::Int(salt)],
}
}
/// Condensed replacements for one string's chunks: a `ldc` per chunk
/// (bootstrap indices `base..base+n`) joined by `String.concat`.
pub(super) fn sequence(base: u16, chunks: usize) -> Vec<Insn> {
let mut out = Vec::with_capacity(chunks * 2);
for position in 0..chunks {
out.push(Insn::Const(Const::Dynamic {
name: "_".to_string(),
descriptor: "Ljava/lang/String;".to_string(),
bootstrap: base + position as u16,
}));
if position > 0 {
out.push(concat());
}
}
out
}
/// `String.concat` - the same chunk join the direct path uses.
fn concat() -> Insn {
Insn::Invoke {
kind: noxis_classfile::InvokeKind::Virtual,
owner: "java/lang/String".to_string(),
name: "concat".to_string(),
descriptor: "(Ljava/lang/String;)Ljava/lang/String;".to_string(),
itf: false,
}
}

View file

@ -4,6 +4,7 @@
//! (KDF or RNG stream - see `derive_key`). The decoder is generated once per //! (KDF or RNG stream - see `derive_key`). The decoder is generated once per
//! program and only if at least one string was encrypted. //! program and only if at least one string was encrypted.
mod condy;
mod crypt; mod crypt;
#[cfg(test)] #[cfg(test)]
@ -12,7 +13,9 @@ mod tests;
use std::collections::BTreeMap; use std::collections::BTreeMap;
use noxis_analysis::{ClassHierarchy, JdkHierarchy}; use noxis_analysis::{ClassHierarchy, JdkHierarchy};
use noxis_classfile::{Const, Insn, MAX_METHOD_INSNS, Program, compute_frames}; use noxis_classfile::{
Attribute, BootstrapMethod, Const, Insn, MAX_METHOD_INSNS, Program, compute_frames,
};
use noxis_core::{Context, CoreError, NameStyle, Pass, PassId, PassReport, derive_seed}; use noxis_core::{Context, CoreError, NameStyle, Pass, PassId, PassReport, derive_seed};
use noxis_runtime::{Scheme, decoder_class, decoder_owner, encrypt}; use noxis_runtime::{Scheme, decoder_class, decoder_owner, encrypt};
@ -69,6 +72,17 @@ impl Pass for StringsPass {
let owner = decoder_name(ctx, program); let owner = decoder_name(ctx, program);
let mut chunk_counter = 0usize; let mut chunk_counter = 0usize;
let mut salt_counter = 0u64; let mut salt_counter = 0u64;
// Condy mode: per-class next bootstrap index + accumulated entries
// (appended to the class attribute after the rewrite below).
let mut bsm_next: BTreeMap<usize, u16> = BTreeMap::new();
let mut bsm_entries: BTreeMap<usize, Vec<BootstrapMethod>> = BTreeMap::new();
if cfg.condy {
for (ci, class) in program.classes.iter().enumerate() {
if condy::supported(class) {
bsm_next.insert(ci, condy::base_index(class));
}
}
}
let mut replacements: BTreeMap<(usize, usize, usize), Vec<Insn>> = BTreeMap::new(); let mut replacements: BTreeMap<(usize, usize, usize), Vec<Insn>> = BTreeMap::new();
for (ci, mi, ii, text) in &targets { for (ci, mi, ii, text) in &targets {
let mut parts = Vec::new(); let mut parts = Vec::new();
@ -80,7 +94,19 @@ impl Pass for StringsPass {
parts.push((blob.cipher, salt)); parts.push((blob.cipher, salt));
chunk_counter += 1; chunk_counter += 1;
} }
replacements.insert((*ci, *mi, *ii), decode_sequence(&owner, &parts)); if let Some(next) = bsm_next.get_mut(ci) {
let base = *next;
for (cipher, salt) in &parts {
bsm_entries
.entry(*ci)
.or_default()
.push(condy::entry(&owner, cipher, *salt));
}
*next = base + parts.len() as u16;
replacements.insert((*ci, *mi, *ii), condy::sequence(base, parts.len()));
} else {
replacements.insert((*ci, *mi, *ii), decode_sequence(&owner, &parts));
}
} }
let mut mutated: Vec<(usize, usize)> = Vec::new(); let mut mutated: Vec<(usize, usize)> = Vec::new();
let mut applied = 0usize; let mut applied = 0usize;
@ -126,6 +152,21 @@ impl Pass for StringsPass {
decoder_class(&owner, master, &scheme_names).map_err(|e| pass_error(e.to_string()))?; decoder_class(&owner, master, &scheme_names).map_err(|e| pass_error(e.to_string()))?;
program.classes.push(decoder); program.classes.push(decoder);
ctx.note(format!("string decoder: {owner}")); ctx.note(format!("string decoder: {owner}"));
// Phase 3b: condy bootstrap entries land on their classes (appended
// after existing lambdas - the indices computed above assumed that).
for (ci, entries) in bsm_entries {
let class = program
.classes
.get_mut(ci)
.ok_or_else(|| pass_error("class vanished"))?;
match class.attributes.iter_mut().find_map(|a| match a {
Attribute::BootstrapMethods(list) => Some(list),
_ => None,
}) {
Some(list) => list.extend(entries),
None => class.attributes.push(Attribute::BootstrapMethods(entries)),
}
}
// Phase 4: frames of the touched methods. // Phase 4: frames of the touched methods.
let hierarchy = ClassHierarchy::build(program); let hierarchy = ClassHierarchy::build(program);
let frames_hierarchy = JdkHierarchy::wrap(&hierarchy); let frames_hierarchy = JdkHierarchy::wrap(&hierarchy);

View file

@ -0,0 +1,201 @@
//! Condy-mode tests: `CONSTANT_Dynamic` emission, version gate, attribute
//! merging.
#![allow(clippy::expect_used, clippy::unwrap_used)]
use noxis_classfile::{Attribute, BootstrapMethod, Const, Insn, Version};
use noxis_core::{Config, Context, Pass, StringsConfig};
use noxis_runtime::{Scheme, encrypt};
use super::super::StringsPass;
use super::sample;
/// Condensed mode: strings become `CONSTANT_Dynamic` resolved by the
/// decoder's bootstrap method - a concrete-execution deobfuscator sees no
/// decrypt call to follow (todo.md, 2026-10-14: 0 condy changes from SSVM).
#[test]
fn condy_mode_emits_dynamic_constants() {
let mut program = sample();
let config = Config {
seed: Some(7),
strings: StringsConfig {
condy: true,
..StringsConfig::default()
},
..Config::default()
};
let mut ctx = Context::new(config);
let report = StringsPass.apply(&mut ctx, &mut program).expect("strings");
assert_eq!(report.transformed, 2);
let class = &program.classes[0];
let code = class.methods[0].code.as_ref().expect("code");
// Both encrypted strings are condy constants, no direct decoder calls.
let dynamics: Vec<&Const> = code
.insns
.iter()
.filter_map(|insn| match insn {
Insn::Const(c @ Const::Dynamic { .. }) => Some(c),
_ => None,
})
.collect();
assert_eq!(dynamics.len(), 3, "1 chunk + 2 chunks: {code:?}");
for value in &dynamics {
let Const::Dynamic { descriptor, .. } = value else {
unreachable!()
};
assert_eq!(descriptor, "Ljava/lang/String;");
}
assert!(
!code
.insns
.iter()
.any(|insn| matches!(insn, Insn::Invoke { name, .. } if name == "d")),
"condy mode must not emit direct decoder calls: {code:?}"
);
// The bootstrap entries live on the class and point at the decoder's `b`.
let decoder = program
.classes
.iter()
.find(|c| c.methods.iter().any(|m| m.name == "b"))
.expect("decoder with bootstrap method");
let Attribute::BootstrapMethods(bsm) = class
.attributes
.iter()
.find(|a| matches!(a, Attribute::BootstrapMethods(_)))
.expect("BootstrapMethods attribute")
else {
unreachable!()
};
assert_eq!(bsm.len(), 3, "one entry per chunk");
for entry in bsm {
assert_eq!(entry.kind, 6, "REF_invokeStatic");
assert_eq!(entry.owner, decoder.this_class);
assert_eq!(entry.name, "b");
assert!(
matches!(entry.args.as_slice(), [Const::String(_), Const::Int(_)]),
"args are cipher + salt: {:?}",
entry.args
);
}
// Bootstrap indices in the constants match the entry positions.
let indices: Vec<u16> = dynamics
.iter()
.map(|c| {
let Const::Dynamic { bootstrap, .. } = c else {
unreachable!()
};
*bootstrap
})
.collect();
assert_eq!(indices, vec![0, 1, 2]);
// Ciphertext is still the direct encryption of the originals.
let schemes = [Scheme::XorSplitmix, Scheme::XteaCtr];
let originals = ["hello world".to_string(), "y".repeat(50), "y".repeat(50)];
let ciphers: Vec<String> = bsm
.iter()
.map(|e| match e.args[0] {
Const::String(ref s) => s.clone(),
_ => unreachable!(),
})
.collect();
let salts: Vec<i32> = bsm
.iter()
.map(|e| match e.args[1] {
Const::Int(s) => s,
_ => unreachable!(),
})
.collect();
for ((cipher, salt), original) in ciphers.iter().zip(&salts).zip(originals) {
let scheme = schemes[*salt as usize % schemes.len()];
assert_eq!(cipher, &encrypt(scheme, 7, *salt, &original).cipher);
}
}
/// Condy is a Java 11+ feature (classfile 55): older classes keep the direct
/// decoder calls even when the mode is on.
#[test]
fn condy_falls_back_for_old_class_versions() {
let mut program = sample();
program.classes[0].version = Version::JAVA_8;
let config = Config {
seed: Some(7),
strings: StringsConfig {
condy: true,
..StringsConfig::default()
},
..Config::default()
};
let mut ctx = Context::new(config);
StringsPass.apply(&mut ctx, &mut program).expect("strings");
let code = program.classes[0].methods[0].code.as_ref().expect("code");
assert!(
code.insns
.iter()
.any(|insn| matches!(insn, Insn::Invoke { name, .. } if name == "d")),
"pre-11 class must keep direct calls: {code:?}"
);
assert!(
!code
.insns
.iter()
.any(|insn| matches!(insn, Insn::Const(Const::Dynamic { .. }))),
"no condy constants below classfile 55: {code:?}"
);
}
/// A class that already has `BootstrapMethods` (Kotlin lambdas) keeps its
/// entries: new condy ones are appended and referenced by the right index.
#[test]
fn condy_appends_to_existing_bootstrap_methods() {
let mut program = sample();
let existing = BootstrapMethod {
kind: 6,
owner: "java/lang/invoke/StringConcatFactory".to_string(),
name: "makeConcatWithConstants".to_string(),
descriptor: "([Ljava/lang/Object;)Ljava/lang/String;".to_string(),
itf: false,
args: vec![Const::String("\u{1}".to_string())],
};
program.classes[0]
.attributes
.push(Attribute::BootstrapMethods(vec![existing]));
let config = Config {
seed: Some(7),
strings: StringsConfig {
condy: true,
..StringsConfig::default()
},
..Config::default()
};
let mut ctx = Context::new(config);
StringsPass.apply(&mut ctx, &mut program).expect("strings");
let class = &program.classes[0];
let Attribute::BootstrapMethods(bsm) = class
.attributes
.iter()
.find(|a| matches!(a, Attribute::BootstrapMethods(_)))
.expect("BootstrapMethods attribute")
else {
unreachable!()
};
// 1 lambda entry + 3 chunk entries.
assert_eq!(bsm.len(), 4);
assert_eq!(bsm[0].name, "makeConcatWithConstants");
let code = class.methods[0].code.as_ref().expect("code");
let indices: Vec<u16> = code
.insns
.iter()
.filter_map(|insn| match insn {
Insn::Const(Const::Dynamic { bootstrap, .. }) => Some(*bootstrap),
_ => None,
})
.collect();
assert_eq!(
indices,
vec![1, 2, 3],
"condy entries start after the lambda"
);
}

View file

@ -2,6 +2,8 @@
#![allow(clippy::expect_used, clippy::unwrap_used)] #![allow(clippy::expect_used, clippy::unwrap_used)]
mod condy;
use noxis_classfile::{ClassBuilder, Code, Const, Insn, MAX_METHOD_INSNS, Program, Version}; use noxis_classfile::{ClassBuilder, Code, Const, Insn, MAX_METHOD_INSNS, Program, Version};
use noxis_core::{Config, Context}; use noxis_core::{Config, Context};
use noxis_runtime::{Scheme, encrypt}; use noxis_runtime::{Scheme, encrypt};
@ -10,7 +12,7 @@ use super::StringsPass;
use super::crypt::{decode_sequence, salt_for_scheme, split_chunks}; use super::crypt::{decode_sequence, salt_for_scheme, split_chunks};
use noxis_core::Pass; use noxis_core::Pass;
fn sample() -> Program { pub(super) fn sample() -> Program {
let code = Code { let code = Code {
max_stack: 1, max_stack: 1,
max_locals: 0, max_locals: 0,

View file

@ -31,6 +31,11 @@ use kdf::{Gen, c_int, c_long, c_str, call, load, store};
pub const DECODER_METHOD: &str = "d"; pub const DECODER_METHOD: &str = "d";
/// Дескриптор метода декодера. /// Дескриптор метода декодера.
pub const DECODER_DESC: &str = "(Ljava/lang/String;I)Ljava/lang/String;"; pub const DECODER_DESC: &str = "(Ljava/lang/String;I)Ljava/lang/String;";
/// Имя bootstrap-метода для `CONSTANT_Dynamic` (condy-строки).
pub const BOOTSTRAP_METHOD: &str = "b";
/// Дескриптор bootstrap-метода: стандартный префикс BSM
/// (`Lookup`, `name`, `type`) плюс наши аргументы `cipher` и `salt`.
pub const BOOTSTRAP_DESC: &str = "(Ljava/lang/invoke/MethodHandles$Lookup;Ljava/lang/String;Ljava/lang/Class;Ljava/lang/String;I)Ljava/lang/String;";
/// Ошибка генерации рантайма. /// Ошибка генерации рантайма.
#[derive(Debug, thiserror::Error)] #[derive(Debug, thiserror::Error)]
@ -78,9 +83,38 @@ pub fn decoder_class(
major: 49, major: 49,
}) })
.method_with_code(0x0009, DECODER_METHOD, DECODER_DESC, code) .method_with_code(0x0009, DECODER_METHOD, DECODER_DESC, code)
.method_with_code(
0x0009,
BOOTSTRAP_METHOD,
BOOTSTRAP_DESC,
bootstrap_code(internal_name),
)
.build()) .build())
} }
/// Тело bootstrap-метода: `b(lookup, name, type, cipher, salt) = d(cipher, salt)`.
///
/// JVM вызывает его сама при первом `ldc` condy-константы и кеширует
/// результат - метод должен быть `public static` и возвращать значение
/// дескриптора константы (`Ljava/lang/String;`).
fn bootstrap_code(owner: &str) -> Code {
Code {
max_stack: 2,
// Слоты 0-2 (lookup/name/type) заняты стандартным префиксом BSM,
// cipher и salt живут в 3 и 4.
max_locals: 5,
insns: vec![
load(3, SlotKind::Ref),
load(4, SlotKind::Int),
call(owner, DECODER_METHOD, DECODER_DESC, InvokeKind::Static),
Insn::AReturn,
],
exception_table: Vec::new(),
frames: Vec::new(),
attributes: Vec::new(),
}
}
/// Имя класса-декодера для прогона: детерминированно от сида, но выглядит /// Имя класса-декодера для прогона: детерминированно от сида, но выглядит
/// как обычный обфусцированный класс. /// как обычный обфусцированный класс.
pub fn decoder_owner(seed_name: &str) -> String { pub fn decoder_owner(seed_name: &str) -> String {

View file

@ -6,5 +6,7 @@ pub mod codec;
pub mod decoder; pub mod decoder;
pub mod scheme; pub mod scheme;
pub use decoder::{DECODER_DESC, DECODER_METHOD, decoder_class, decoder_owner}; pub use decoder::{
BOOTSTRAP_DESC, BOOTSTRAP_METHOD, DECODER_DESC, DECODER_METHOD, decoder_class, decoder_owner,
};
pub use scheme::{Blob, Scheme, decrypt, encrypt}; pub use scheme::{Blob, Scheme, decrypt, encrypt};

View file

@ -0,0 +1,132 @@
//! Condy-приёмка: шифруем в Rust, расшифровывает настоящая JVM через
//! bootstrap-метод декодера (`CONSTANT_Dynamic`).
//!
//! Это приёмка приоритета 1 из todo.md (2026-10-14): sandbox-деобфускатор
//! (narumii/SSVM) не резолвит condy вообще - 0 изменений на condy-классе,
//! тогда как прямой вызов декодера он исполняет и снимает строку. Метод `b`
//! живёт в декодере и вызывается самой JVM в момент `ldc`.
#![allow(clippy::expect_used, clippy::unwrap_used)]
use std::path::PathBuf;
use noxis_classfile::{Attribute, BootstrapMethod, ClassBuilder, Code, Const, Insn, format};
use noxis_runtime::{BOOTSTRAP_DESC, BOOTSTRAP_METHOD, Scheme, decoder_class, encrypt};
const MASTER: u64 = 0x1234_5678_9ABC_DEF0;
const DECODER: &str = "test/CondDecoder";
const CALLER: &str = "test/CondCall";
/// Строки обоих схем - тот же набор, что у прямого вызова.
fn samples() -> Vec<(String, Scheme)> {
vec![
("hi".to_string(), Scheme::XorSplitmix),
("Hello, мир!".to_string(), Scheme::XorSplitmix),
("x".repeat(100), Scheme::XorSplitmix),
("hi".to_string(), Scheme::XteaCtr),
("Hello, мир!".to_string(), Scheme::XteaCtr),
("x".repeat(100), Scheme::XteaCtr),
]
}
/// Класс-вызыватель condy-режима: `main` печатает `ldc CONSTANT_Dynamic`.
///
/// Ровно то, что эмитит пасс `strings` при `condy = true`: по одной
/// bootstrap-записи на чанк (cipher + salt как статические аргументы),
/// константа со стандартным дескриптором `Ljava/lang/String;`.
fn condy_caller_class(parts: &[(String, i32)]) -> noxis_classfile::ClassFile {
let mut insns = Vec::new();
let mut bootstrap: Vec<BootstrapMethod> = Vec::new();
for (cipher, salt) in parts {
bootstrap.push(BootstrapMethod {
kind: 6, // REF_invokeStatic
owner: DECODER.to_string(),
name: BOOTSTRAP_METHOD.to_string(),
descriptor: BOOTSTRAP_DESC.to_string(),
itf: false,
args: vec![Const::String(cipher.clone()), Const::Int(*salt)],
});
insns.push(Insn::GetStatic {
owner: "java/lang/System".to_string(),
name: "out".to_string(),
descriptor: "Ljava/io/PrintStream;".to_string(),
});
insns.push(Insn::Const(Const::Dynamic {
name: "_".to_string(),
descriptor: "Ljava/lang/String;".to_string(),
bootstrap: (bootstrap.len() - 1) as u16,
}));
insns.push(Insn::Invoke {
kind: noxis_classfile::InvokeKind::Virtual,
owner: "java/io/PrintStream".to_string(),
name: "println".to_string(),
descriptor: "(Ljava/lang/String;)V".to_string(),
itf: false,
});
}
insns.push(Insn::Return(None));
let code = Code {
max_stack: 2,
max_locals: 1,
insns,
exception_table: Vec::new(),
frames: Vec::new(),
attributes: Vec::new(),
};
ClassBuilder::new(CALLER, None)
.attribute(Attribute::BootstrapMethods(bootstrap))
.method_with_code(0x0009, "main", "([Ljava/lang/String;)V", code)
.build()
}
#[test]
fn condy_bootstrap_executes_under_java() {
if std::process::Command::new("java")
.arg("-version")
.output()
.is_err()
{
eprintln!("SKIP: нет java");
return;
}
let mut parts = Vec::new();
let mut expected = Vec::new();
for (index, (text, scheme)) in samples().iter().enumerate() {
let salt = (index as i32)
.saturating_mul(2)
.saturating_add(match scheme {
Scheme::XorSplitmix => 0,
Scheme::XteaCtr => 1,
});
let blob = encrypt(*scheme, MASTER, salt, text);
parts.push((blob.cipher, salt));
expected.push(text.to_string());
}
let decoder = decoder_class(DECODER, MASTER, &["xor-splitmix", "xtea-ctr"]).expect("decoder");
let caller = condy_caller_class(&parts);
let dir: PathBuf = std::env::temp_dir().join(format!("noxis-condy-{}", std::process::id()));
for (name, class) in [(DECODER, &decoder), (CALLER, &caller)] {
let mut path = dir.clone();
for part in name.split('/') {
path.push(part);
}
path.set_extension("class");
std::fs::create_dir_all(path.parent().expect("parent")).expect("mkdir");
std::fs::write(&path, format::write(class).expect("write")).expect("write class");
}
let out = std::process::Command::new("java")
.arg("-Xverify:all")
.arg("-cp")
.arg(&dir)
.arg(CALLER.replace('/', "."))
.output()
.expect("запуск java");
let stdout = String::from_utf8_lossy(&out.stdout);
let stderr = String::from_utf8_lossy(&out.stderr);
let _ = std::fs::remove_dir_all(&dir);
assert!(out.status.success(), "java упал:\n{stderr}\n{stdout}");
let lines: Vec<&str> = stdout.lines().collect();
let want: Vec<&str> = expected.iter().map(String::as_str).collect();
assert_eq!(lines, want, "condy-расшифровка не сошлась");
}