passes,core: debug fakes - line numbers and source removal
DebugConfig gains fake_line_numbers (deterministic upward walk replacing LineNumberTable rows when stripping is off; strip wins) and strip_source_file (drops SourceFile entirely, beating spoof). Debug tests moved to passes/tests/debug_info.rs with new cases for both flags and their precedence.
This commit is contained in:
parent
2cbb9db31e
commit
6a68c9d69b
3 changed files with 258 additions and 135 deletions
|
|
@ -175,10 +175,15 @@ impl Default for NoiseConfig {
|
||||||
pub struct DebugConfig {
|
pub struct DebugConfig {
|
||||||
/// Strip `LineNumberTable`.
|
/// Strip `LineNumberTable`.
|
||||||
pub strip_line_numbers: bool,
|
pub strip_line_numbers: bool,
|
||||||
|
/// Replace line numbers with plausible-but-wrong ones. Applies only
|
||||||
|
/// when `strip_line_numbers` is off (stripping wins).
|
||||||
|
pub fake_line_numbers: bool,
|
||||||
/// Strip `LocalVariableTable`.
|
/// Strip `LocalVariableTable`.
|
||||||
pub strip_local_variables: bool,
|
pub strip_local_variables: bool,
|
||||||
/// Replace `SourceFile` with a fake name.
|
/// Replace `SourceFile` with a fake name.
|
||||||
pub spoof_source_file: bool,
|
pub spoof_source_file: bool,
|
||||||
|
/// Remove `SourceFile` entirely instead of spoofing it.
|
||||||
|
pub strip_source_file: bool,
|
||||||
/// Strip `Signature`/`InnerClasses`/`EnclosingMethod`.
|
/// Strip `Signature`/`InnerClasses`/`EnclosingMethod`.
|
||||||
pub strip_signatures: bool,
|
pub strip_signatures: bool,
|
||||||
}
|
}
|
||||||
|
|
@ -187,8 +192,10 @@ impl Default for DebugConfig {
|
||||||
fn default() -> Self {
|
fn default() -> Self {
|
||||||
Self {
|
Self {
|
||||||
strip_line_numbers: true,
|
strip_line_numbers: true,
|
||||||
|
fake_line_numbers: false,
|
||||||
strip_local_variables: true,
|
strip_local_variables: true,
|
||||||
spoof_source_file: true,
|
spoof_source_file: true,
|
||||||
|
strip_source_file: false,
|
||||||
strip_signatures: false,
|
strip_signatures: false,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,26 +1,28 @@
|
||||||
//! Пасс отладочной информации: удаление и ложная подмена.
|
//! The debug-info pass: removal and false replacement.
|
||||||
//!
|
//!
|
||||||
//! Источник требований - `info/evo-weaknesses.md` §3: у разобранного
|
//! Requirements come from `info/evo-weaknesses.md` §3: the dissected sample
|
||||||
//! образца оригинальные `SourceFile` у 160 классов, 10 218 `LineNumberTable`
|
//! kept original `SourceFile`s on 160 classes, 10 218 `LineNumberTable`s and
|
||||||
//! и имена локальных переменных. Это дешевле всего чинить и дороже всего
|
//! local variable names. This is the cheapest thing to fix and the most
|
||||||
//! отдавать.
|
//! expensive to give away.
|
||||||
//!
|
//!
|
||||||
//! Важно: удалять недостаточно. Инструменты (JADX/CFR) верят отладке сильнее,
|
//! Removal alone is not enough. Tools (JADX/CFR) trust debug info more than
|
||||||
//! чем байткоду, поэтому `spoof_source_file` подставляет **ложное** имя -
|
//! bytecode, so `spoof_source_file` substitutes a **false** name -
|
||||||
//! консистентное, но не связанное с реальным исходником.
|
//! consistent, but unrelated to the real sources. `fake_line_numbers` does
|
||||||
|
//! the same to line numbers (a plausible walk instead of the truth), and
|
||||||
|
//! `strip_source_file` drops the attribute entirely.
|
||||||
|
|
||||||
use noxis_classfile::{Attribute, ClassFile, Code, Method, Program};
|
use noxis_classfile::{Attribute, ClassFile, Code, Method, Program};
|
||||||
use noxis_core::{Context, CoreError, DebugConfig, Pass, PassId, PassReport};
|
use noxis_core::{Context, CoreError, DebugConfig, Pass, PassId, PassReport, Rng};
|
||||||
|
|
||||||
use crate::namegen::NameGenerator;
|
use crate::namegen::NameGenerator;
|
||||||
|
|
||||||
/// Атрибут уровня класса/метода, который мы выкидываем как «сырой».
|
/// A class/method-level attribute dropped as "raw".
|
||||||
const ENCLOSING_METHOD: &str = "EnclosingMethod";
|
const ENCLOSING_METHOD: &str = "EnclosingMethod";
|
||||||
|
|
||||||
/// Атрибут уровня метода с именами параметров (`-parameters`).
|
/// A method-level attribute holding parameter names (`-parameters`).
|
||||||
const METHOD_PARAMETERS: &str = "MethodParameters";
|
const METHOD_PARAMETERS: &str = "MethodParameters";
|
||||||
|
|
||||||
/// Пасс отладочной информации.
|
/// The debug-info pass.
|
||||||
#[derive(Debug, Default, Clone, Copy)]
|
#[derive(Debug, Default, Clone, Copy)]
|
||||||
pub struct DebugInfoPass;
|
pub struct DebugInfoPass;
|
||||||
|
|
||||||
|
|
@ -34,11 +36,14 @@ impl Pass for DebugInfoPass {
|
||||||
let style = ctx.config.rename.class_style;
|
let style = ctx.config.rename.class_style;
|
||||||
let mut source_names =
|
let mut source_names =
|
||||||
NameGenerator::new(ctx.rng("debug/source_file"), style).for_classes();
|
NameGenerator::new(ctx.rng("debug/source_file"), style).for_classes();
|
||||||
|
let mut line_rng = ctx.rng("debug/lines");
|
||||||
|
|
||||||
let mut stripped = 0usize;
|
let mut stripped = 0usize;
|
||||||
let mut spoofed = 0usize;
|
let mut spoofed = 0usize;
|
||||||
|
let mut faked = 0usize;
|
||||||
|
|
||||||
for class in &mut program.classes {
|
for class in &mut program.classes {
|
||||||
|
stripped += strip_source_file(class, &cfg);
|
||||||
spoofed += spoof_source_file(class, &mut source_names, &cfg);
|
spoofed += spoof_source_file(class, &mut source_names, &cfg);
|
||||||
stripped += strip_signatures(&mut class.attributes, &cfg);
|
stripped += strip_signatures(&mut class.attributes, &cfg);
|
||||||
|
|
||||||
|
|
@ -47,6 +52,7 @@ impl Pass for DebugInfoPass {
|
||||||
stripped += strip_parameters(method, &cfg);
|
stripped += strip_parameters(method, &cfg);
|
||||||
if let Some(code) = method.code.as_mut() {
|
if let Some(code) = method.code.as_mut() {
|
||||||
stripped += strip_code(code, &cfg);
|
stripped += strip_code(code, &cfg);
|
||||||
|
faked += fake_line_numbers(code, &mut line_rng, &cfg);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -57,29 +63,32 @@ impl Pass for DebugInfoPass {
|
||||||
|
|
||||||
let mut notes = Vec::new();
|
let mut notes = Vec::new();
|
||||||
if stripped > 0 {
|
if stripped > 0 {
|
||||||
notes.push(format!("удалено атрибутов: {stripped}"));
|
notes.push(format!("attributes removed: {stripped}"));
|
||||||
}
|
}
|
||||||
if spoofed > 0 {
|
if spoofed > 0 {
|
||||||
notes.push(format!("подменено SourceFile: {spoofed}"));
|
notes.push(format!("SourceFile spoofed: {spoofed}"));
|
||||||
|
}
|
||||||
|
if faked > 0 {
|
||||||
|
notes.push(format!("line numbers faked: {faked}"));
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(PassReport {
|
Ok(PassReport {
|
||||||
pass: PassId::DebugInfo,
|
pass: PassId::DebugInfo,
|
||||||
transformed: stripped + spoofed,
|
transformed: stripped + spoofed + faked,
|
||||||
skipped: 0,
|
skipped: 0,
|
||||||
notes,
|
notes,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Оставляет только атрибуты, для которых `keep` вернул `true`.
|
/// Keep only the attributes for which `keep` returns true.
|
||||||
fn retain(attrs: &mut Vec<Attribute>, keep: impl FnMut(&Attribute) -> bool) -> usize {
|
fn retain(attrs: &mut Vec<Attribute>, keep: impl FnMut(&Attribute) -> bool) -> usize {
|
||||||
let before = attrs.len();
|
let before = attrs.len();
|
||||||
attrs.retain(keep);
|
attrs.retain(keep);
|
||||||
before - attrs.len()
|
before - attrs.len()
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Тела методов: `LineNumberTable`, `LocalVariableTable`.
|
/// Method bodies: `LineNumberTable`, `LocalVariableTable`.
|
||||||
fn strip_code(code: &mut Code, cfg: &DebugConfig) -> usize {
|
fn strip_code(code: &mut Code, cfg: &DebugConfig) -> usize {
|
||||||
retain(&mut code.attributes, |attr| match attr {
|
retain(&mut code.attributes, |attr| match attr {
|
||||||
Attribute::LineNumberTable(_) => !cfg.strip_line_numbers,
|
Attribute::LineNumberTable(_) => !cfg.strip_line_numbers,
|
||||||
|
|
@ -90,7 +99,7 @@ fn strip_code(code: &mut Code, cfg: &DebugConfig) -> usize {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Уровень класса/метода/поля: `Signature`, `InnerClasses`, `EnclosingMethod`.
|
/// Class/method/field level: `Signature`, `InnerClasses`, `EnclosingMethod`.
|
||||||
fn strip_signatures(attrs: &mut Vec<Attribute>, cfg: &DebugConfig) -> usize {
|
fn strip_signatures(attrs: &mut Vec<Attribute>, cfg: &DebugConfig) -> usize {
|
||||||
if !cfg.strip_signatures {
|
if !cfg.strip_signatures {
|
||||||
return 0;
|
return 0;
|
||||||
|
|
@ -103,7 +112,7 @@ fn strip_signatures(attrs: &mut Vec<Attribute>, cfg: &DebugConfig) -> usize {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Имена параметров метода (`MethodParameters`).
|
/// Method parameter names (`MethodParameters`).
|
||||||
fn strip_parameters(method: &mut Method, cfg: &DebugConfig) -> usize {
|
fn strip_parameters(method: &mut Method, cfg: &DebugConfig) -> usize {
|
||||||
if !cfg.strip_local_variables {
|
if !cfg.strip_local_variables {
|
||||||
return 0;
|
return 0;
|
||||||
|
|
@ -115,7 +124,40 @@ fn strip_parameters(method: &mut Method, cfg: &DebugConfig) -> usize {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Подменяет `SourceFile` ложным, но правдоподобным именем.
|
/// Replace `LineNumberTable` rows with plausible-but-wrong numbers.
|
||||||
|
///
|
||||||
|
/// Runs only when stripping is off (a removed table has nothing to fake):
|
||||||
|
/// a deterministic upward walk, so stack traces still look like traces
|
||||||
|
/// instead of pointing at the real lines.
|
||||||
|
fn fake_line_numbers(code: &mut Code, rng: &mut Rng, cfg: &DebugConfig) -> usize {
|
||||||
|
if !cfg.fake_line_numbers || cfg.strip_line_numbers {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
let mut faked = 0usize;
|
||||||
|
let mut line = 1 + rng.below(200) as u16;
|
||||||
|
for attr in &mut code.attributes {
|
||||||
|
if let Attribute::LineNumberTable(rows) = attr {
|
||||||
|
for row in rows.iter_mut() {
|
||||||
|
row.line = line;
|
||||||
|
line = line.saturating_add(1 + rng.below(8) as u16);
|
||||||
|
faked += 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
faked
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Remove `SourceFile` entirely instead of spoofing it.
|
||||||
|
fn strip_source_file(class: &mut ClassFile, cfg: &DebugConfig) -> usize {
|
||||||
|
if !cfg.strip_source_file {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
retain(&mut class.attributes, |attr| {
|
||||||
|
!matches!(attr, Attribute::SourceFile(_))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Replace `SourceFile` with a false but plausible name.
|
||||||
fn spoof_source_file(class: &mut ClassFile, names: &mut NameGenerator, cfg: &DebugConfig) -> usize {
|
fn spoof_source_file(class: &mut ClassFile, names: &mut NameGenerator, cfg: &DebugConfig) -> usize {
|
||||||
if !cfg.spoof_source_file {
|
if !cfg.spoof_source_file {
|
||||||
return 0;
|
return 0;
|
||||||
|
|
@ -129,118 +171,3 @@ fn spoof_source_file(class: &mut ClassFile, names: &mut NameGenerator, cfg: &Deb
|
||||||
}
|
}
|
||||||
replaced
|
replaced
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
|
||||||
mod tests {
|
|
||||||
#![allow(clippy::expect_used, clippy::unwrap_used)]
|
|
||||||
|
|
||||||
use noxis_classfile::{ClassBuilder, Insn, LineNumber, LocalVariable, Resource, Version};
|
|
||||||
use noxis_core::{Config, Context, RenameConfig};
|
|
||||||
|
|
||||||
use super::*;
|
|
||||||
|
|
||||||
fn sample() -> Program {
|
|
||||||
let code = Code {
|
|
||||||
max_stack: 0,
|
|
||||||
max_locals: 1,
|
|
||||||
insns: vec![Insn::Return(None)],
|
|
||||||
exception_table: Vec::new(),
|
|
||||||
frames: Vec::new(),
|
|
||||||
attributes: vec![
|
|
||||||
Attribute::LineNumberTable(vec![LineNumber { start: 0, line: 12 }]),
|
|
||||||
Attribute::LocalVariableTable(vec![LocalVariable {
|
|
||||||
start: 0,
|
|
||||||
end: 0,
|
|
||||||
name: "this".into(),
|
|
||||||
descriptor: "Ldemo/App;".into(),
|
|
||||||
index: 0,
|
|
||||||
}]),
|
|
||||||
],
|
|
||||||
};
|
|
||||||
|
|
||||||
let mut class = ClassBuilder::new("demo/App", Some("java/lang/Object"))
|
|
||||||
.version(Version::JAVA_21)
|
|
||||||
.method_with_code(0x0001, "run", "()V", code)
|
|
||||||
.attribute(Attribute::SourceFile("RealApp.java".into()))
|
|
||||||
.attribute(Attribute::Signature("Ljava/lang/Object;".into()))
|
|
||||||
.attribute(Attribute::InnerClasses(Vec::new()))
|
|
||||||
.build();
|
|
||||||
class.methods[0].attributes.push(Attribute::Raw {
|
|
||||||
name: METHOD_PARAMETERS.into(),
|
|
||||||
data: vec![],
|
|
||||||
});
|
|
||||||
|
|
||||||
Program {
|
|
||||||
classes: vec![class],
|
|
||||||
resources: vec![Resource {
|
|
||||||
path: "app.json".into(),
|
|
||||||
data: br#"{}"#.to_vec(),
|
|
||||||
}],
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn run(program: &mut Program, debug: DebugConfig) -> PassReport {
|
|
||||||
let config = Config {
|
|
||||||
seed: Some(7),
|
|
||||||
debug,
|
|
||||||
rename: RenameConfig {
|
|
||||||
class_style: noxis_core::NameStyle::ShortLatin,
|
|
||||||
..RenameConfig::default()
|
|
||||||
},
|
|
||||||
..Config::default()
|
|
||||||
};
|
|
||||||
let mut ctx = Context::new(config);
|
|
||||||
DebugInfoPass.apply(&mut ctx, program).expect("debug pass")
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn default_config_strips_and_spoofs() {
|
|
||||||
let mut program = sample();
|
|
||||||
let report = run(&mut program, DebugConfig::default());
|
|
||||||
|
|
||||||
let class = &program.classes[0];
|
|
||||||
let code = class.methods[0].code.as_ref().expect("code");
|
|
||||||
assert!(code.attributes.is_empty(), "атрибуты кода должны уйти");
|
|
||||||
assert!(
|
|
||||||
class.methods[0].attributes.is_empty(),
|
|
||||||
"MethodParameters уходит"
|
|
||||||
);
|
|
||||||
|
|
||||||
match class.attributes[0] {
|
|
||||||
Attribute::SourceFile(ref name) => assert_ne!(name, "RealApp.java"),
|
|
||||||
_ => panic!("SourceFile должен быть первым"),
|
|
||||||
}
|
|
||||||
assert_eq!(class.attributes.len(), 3, "Signature/InnerClasses остаются");
|
|
||||||
assert!(report.transformed > 0);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn signatures_and_parameters_survive_when_disabled() {
|
|
||||||
let debug = DebugConfig {
|
|
||||||
strip_line_numbers: false,
|
|
||||||
strip_local_variables: false,
|
|
||||||
spoof_source_file: false,
|
|
||||||
strip_signatures: false,
|
|
||||||
};
|
|
||||||
let mut program = sample();
|
|
||||||
run(&mut program, debug);
|
|
||||||
|
|
||||||
let class = &program.classes[0];
|
|
||||||
assert_eq!(class.methods[0].code.as_ref().unwrap().attributes.len(), 2);
|
|
||||||
assert_eq!(class.methods[0].attributes.len(), 1);
|
|
||||||
assert_eq!(class.attributes.len(), 3);
|
|
||||||
assert_eq!(
|
|
||||||
class.attributes[0],
|
|
||||||
Attribute::SourceFile("RealApp.java".into())
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn same_seed_gives_the_same_spoofed_name() {
|
|
||||||
let mut first = sample();
|
|
||||||
let mut second = sample();
|
|
||||||
run(&mut first, DebugConfig::default());
|
|
||||||
run(&mut second, DebugConfig::default());
|
|
||||||
assert_eq!(first.classes[0].attributes, second.classes[0].attributes);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
|
||||||
189
crates/noxis-passes/tests/debug_info.rs
Normal file
189
crates/noxis-passes/tests/debug_info.rs
Normal file
|
|
@ -0,0 +1,189 @@
|
||||||
|
//! Debug-info pass tests: strip, spoof, fake lines, source removal.
|
||||||
|
|
||||||
|
#![allow(clippy::expect_used, clippy::unwrap_used)]
|
||||||
|
|
||||||
|
use noxis_classfile::{
|
||||||
|
Attribute, ClassBuilder, Code, Insn, LineNumber, LocalVariable, Program, Resource, Version,
|
||||||
|
};
|
||||||
|
use noxis_core::{Config, Context, DebugConfig, Pass, PassReport, RenameConfig};
|
||||||
|
use noxis_passes::DebugInfoPass;
|
||||||
|
|
||||||
|
fn sample() -> Program {
|
||||||
|
let code = Code {
|
||||||
|
max_stack: 0,
|
||||||
|
max_locals: 1,
|
||||||
|
insns: vec![Insn::Return(None)],
|
||||||
|
exception_table: Vec::new(),
|
||||||
|
frames: Vec::new(),
|
||||||
|
attributes: vec![
|
||||||
|
Attribute::LineNumberTable(vec![
|
||||||
|
LineNumber { start: 0, line: 12 },
|
||||||
|
LineNumber { start: 0, line: 15 },
|
||||||
|
]),
|
||||||
|
Attribute::LocalVariableTable(vec![LocalVariable {
|
||||||
|
start: 0,
|
||||||
|
end: 0,
|
||||||
|
name: "this".into(),
|
||||||
|
descriptor: "Ldemo/App;".into(),
|
||||||
|
index: 0,
|
||||||
|
}]),
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
|
let mut class = ClassBuilder::new("demo/App", Some("java/lang/Object"))
|
||||||
|
.version(Version::JAVA_21)
|
||||||
|
.method_with_code(0x0001, "run", "()V", code)
|
||||||
|
.attribute(Attribute::SourceFile("RealApp.java".into()))
|
||||||
|
.attribute(Attribute::Signature("Ljava/lang/Object;".into()))
|
||||||
|
.attribute(Attribute::InnerClasses(Vec::new()))
|
||||||
|
.build();
|
||||||
|
class.methods[0].attributes.push(Attribute::Raw {
|
||||||
|
name: "MethodParameters".into(),
|
||||||
|
data: vec![],
|
||||||
|
});
|
||||||
|
|
||||||
|
Program {
|
||||||
|
classes: vec![class],
|
||||||
|
resources: vec![Resource {
|
||||||
|
path: "app.json".into(),
|
||||||
|
data: br#"{}"#.to_vec(),
|
||||||
|
}],
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn run(program: &mut Program, debug: DebugConfig) -> PassReport {
|
||||||
|
let config = Config {
|
||||||
|
seed: Some(7),
|
||||||
|
debug,
|
||||||
|
rename: RenameConfig {
|
||||||
|
class_style: noxis_core::NameStyle::ShortLatin,
|
||||||
|
..RenameConfig::default()
|
||||||
|
},
|
||||||
|
..Config::default()
|
||||||
|
};
|
||||||
|
let mut ctx = Context::new(config);
|
||||||
|
DebugInfoPass.apply(&mut ctx, program).expect("debug pass")
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn default_config_strips_and_spoofs() {
|
||||||
|
let mut program = sample();
|
||||||
|
let report = run(&mut program, DebugConfig::default());
|
||||||
|
|
||||||
|
let class = &program.classes[0];
|
||||||
|
let code = class.methods[0].code.as_ref().expect("code");
|
||||||
|
assert!(code.attributes.is_empty(), "code attributes must go");
|
||||||
|
assert!(
|
||||||
|
class.methods[0].attributes.is_empty(),
|
||||||
|
"MethodParameters goes"
|
||||||
|
);
|
||||||
|
|
||||||
|
match class.attributes[0] {
|
||||||
|
Attribute::SourceFile(ref name) => assert_ne!(name, "RealApp.java"),
|
||||||
|
_ => panic!("SourceFile must come first"),
|
||||||
|
}
|
||||||
|
assert_eq!(class.attributes.len(), 3, "Signature/InnerClasses stay");
|
||||||
|
assert!(report.transformed > 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn signatures_and_parameters_survive_when_disabled() {
|
||||||
|
let debug = DebugConfig {
|
||||||
|
strip_line_numbers: false,
|
||||||
|
fake_line_numbers: false,
|
||||||
|
strip_local_variables: false,
|
||||||
|
spoof_source_file: false,
|
||||||
|
strip_source_file: false,
|
||||||
|
strip_signatures: false,
|
||||||
|
};
|
||||||
|
let mut program = sample();
|
||||||
|
run(&mut program, debug);
|
||||||
|
|
||||||
|
let class = &program.classes[0];
|
||||||
|
assert_eq!(class.methods[0].code.as_ref().unwrap().attributes.len(), 2);
|
||||||
|
assert_eq!(class.methods[0].attributes.len(), 1);
|
||||||
|
assert_eq!(class.attributes.len(), 3);
|
||||||
|
assert_eq!(
|
||||||
|
class.attributes[0],
|
||||||
|
Attribute::SourceFile("RealApp.java".into())
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn same_seed_gives_the_same_spoofed_name() {
|
||||||
|
let mut first = sample();
|
||||||
|
let mut second = sample();
|
||||||
|
run(&mut first, DebugConfig::default());
|
||||||
|
run(&mut second, DebugConfig::default());
|
||||||
|
assert_eq!(first.classes[0].attributes, second.classes[0].attributes);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn line_numbers(program: &Program) -> Vec<u16> {
|
||||||
|
let code = program.classes[0].methods[0].code.as_ref().expect("code");
|
||||||
|
code.attributes
|
||||||
|
.iter()
|
||||||
|
.find_map(|attr| match attr {
|
||||||
|
Attribute::LineNumberTable(rows) => Some(rows.iter().map(|r| r.line).collect()),
|
||||||
|
_ => None,
|
||||||
|
})
|
||||||
|
.unwrap_or_default()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Fake lines replace the table instead of removing it, deterministically.
|
||||||
|
#[test]
|
||||||
|
fn fake_line_numbers_replace_not_remove() {
|
||||||
|
let debug = DebugConfig {
|
||||||
|
strip_line_numbers: false,
|
||||||
|
fake_line_numbers: true,
|
||||||
|
..DebugConfig::default()
|
||||||
|
};
|
||||||
|
let mut first = sample();
|
||||||
|
let mut second = sample();
|
||||||
|
run(&mut first, debug.clone());
|
||||||
|
run(&mut second, debug);
|
||||||
|
|
||||||
|
for program in [&first, &second] {
|
||||||
|
let lines = line_numbers(program);
|
||||||
|
assert_eq!(lines.len(), 2, "table must survive with 2 rows");
|
||||||
|
assert_ne!(lines, vec![12, 15], "numbers must change");
|
||||||
|
assert!(lines.iter().all(|l| *l > 0), "numbers stay plausible");
|
||||||
|
}
|
||||||
|
assert_eq!(
|
||||||
|
line_numbers(&first),
|
||||||
|
line_numbers(&second),
|
||||||
|
"same seed, same fake lines"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Stripping the source removes the attribute even with spoofing on.
|
||||||
|
#[test]
|
||||||
|
fn strip_source_file_removes() {
|
||||||
|
let debug = DebugConfig {
|
||||||
|
strip_source_file: true,
|
||||||
|
..DebugConfig::default()
|
||||||
|
};
|
||||||
|
let mut program = sample();
|
||||||
|
run(&mut program, debug);
|
||||||
|
|
||||||
|
assert!(
|
||||||
|
program.classes[0]
|
||||||
|
.attributes
|
||||||
|
.iter()
|
||||||
|
.all(|attr| !matches!(attr, Attribute::SourceFile(_))),
|
||||||
|
"no SourceFile may survive"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Stripping wins over faking: a removed table has nothing to fake.
|
||||||
|
#[test]
|
||||||
|
fn strip_wins_over_fake() {
|
||||||
|
let debug = DebugConfig {
|
||||||
|
strip_line_numbers: true,
|
||||||
|
fake_line_numbers: true,
|
||||||
|
..DebugConfig::default()
|
||||||
|
};
|
||||||
|
let mut program = sample();
|
||||||
|
run(&mut program, debug);
|
||||||
|
|
||||||
|
assert!(line_numbers(&program).is_empty());
|
||||||
|
}
|
||||||
Loading…
Add table
Add a link
Reference in a new issue