classfile: intern constant-pool entries on write (fix LVTT/LVT ClassFormatError)

ristretto's ConstantPool never deduplicates: every add_* pushes a new entry.
HotSpot matches each LocalVariableTypeTable entry to a LocalVariableTable
entry by raw name_index, so when a name (e.g. a local's) was interned twice
the two tables got different indices and the class failed to load:
'ClassFormatError: LVTT entry ... does not match any LVT entry'.

The writer now memoizes every pool builder by its semantic key and rebuilds
composite constants (Class/NameAndType/Fieldref/Methodref/MethodHandle/
Dynamic) from our own utf8/class primitives so ristretto never mints a
shadow Utf8. This also shrinks the pool (no more ldc -> ldc_w widening that
changed code length on a pure round-trip).

Split pool.rs into a pool/ module (refs in pool/refs.rs) to respect the
file-size gate. Regression test: tests/pool asserts the written pool has no
duplicate CONSTANT_Utf8. Verified: pure parse->write of the neoforge jar
(92 classes) now loads under java -Xverify:all with 0 errors.
This commit is contained in:
Loki 2026-10-08 22:55:49 +02:00
parent a2e4c69210
commit f08bf60635
10 changed files with 509 additions and 206 deletions

View file

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

View file

@ -0,0 +1,155 @@
//! Сборка constant pool при записи: строки нашего IR → индексы backend'а.
//!
//! Пул строится с нуля детерминированным обходом (классы, поля, методы,
//! атрибуты, код), поэтому одинаковый IR даёт одинаковые байты. Индексы
//! `Raw`-атрибутов при этом не переносятся — см. `super::attrs`.
//!
//! Backend не дедуплицирует: `add_*` всегда толкает новый элемент. Кэшируем
//! семантические ключи сами — иначе `LocalVariableTable` и
//! `LocalVariableTypeTable` получат разные `name_index` для одного имени, и
//! HotSpot отвергнет класс (`LVTT entry ... does not match any LVT entry`).
//! Порядок обхода детерминирован, поэтому кэш меняет только компактность пула.
//! Референсы (поле/метод/хендл/динамик) — в [`refs`].
use std::collections::HashMap;
use ristretto_classfile::{Constant, ConstantPool};
use crate::error::ClassfileError;
use crate::insn::Const;
mod refs;
/// Строитель пула; живёт только внутри одного `write`.
pub(crate) struct Pool {
pub(super) pool: ConstantPool<'static>,
pub(super) utf8s: HashMap<String, u16>,
pub(super) classes: HashMap<String, u16>,
pub(super) name_types: HashMap<(String, String), u16>,
pub(super) strings: HashMap<String, u16>,
pub(super) method_types: HashMap<String, u16>,
pub(super) field_refs: HashMap<(String, String, String), u16>,
pub(super) method_refs: HashMap<(String, String, String, bool), u16>,
pub(super) handles: HashMap<(u8, String, String, String, bool), u16>,
pub(super) dynamics: HashMap<(u16, String, String, bool), u16>,
}
impl Pool {
pub(crate) fn new() -> Self {
Pool {
pool: ConstantPool::default(),
utf8s: HashMap::new(),
classes: HashMap::new(),
name_types: HashMap::new(),
strings: HashMap::new(),
method_types: HashMap::new(),
field_refs: HashMap::new(),
method_refs: HashMap::new(),
handles: HashMap::new(),
dynamics: HashMap::new(),
}
}
pub(super) fn utf8(&mut self, value: &str) -> Result<u16, ClassfileError> {
if let Some(&index) = self.utf8s.get(value) {
return Ok(index);
}
let index = self.pool.add_utf8(value).map_err(write_err)?;
self.utf8s.insert(value.to_string(), index);
Ok(index)
}
pub(super) fn name_and_type(
&mut self,
name: &str,
descriptor: &str,
) -> Result<u16, ClassfileError> {
let key = (name.to_string(), descriptor.to_string());
if let Some(&index) = self.name_types.get(&key) {
return Ok(index);
}
// Собираем из примитивов, а не `add_name_and_type`: последний толкает
// собственные `Utf8`-строки мимо нашего кэша (дубли в пуле).
let name_index = self.utf8(name)?;
let descriptor_index = self.utf8(descriptor)?;
let index = self
.pool
.add(Constant::NameAndType {
name_index,
descriptor_index,
})
.map_err(write_err)?;
self.name_types.insert(key, index);
Ok(index)
}
pub(super) fn class(&mut self, name: &str) -> Result<u16, ClassfileError> {
if let Some(&index) = self.classes.get(name) {
return Ok(index);
}
let name_index = self.utf8(name)?;
let index = self
.pool
.add(Constant::Class(name_index))
.map_err(write_err)?;
self.classes.insert(name.to_string(), index);
Ok(index)
}
/// Константа для `ldc`/`ldc2_w`; `dynamic` запрещён (только через bootstrap).
pub(super) fn constant(&mut self, value: &Const) -> Result<u16, ClassfileError> {
match value {
Const::Int(v) => self.pool.add_integer(*v).map_err(write_err),
Const::Float(v) => self.pool.add_float(*v).map_err(write_err),
Const::Long(v) => self.pool.add_long(*v).map_err(write_err),
Const::Double(v) => self.pool.add_double(*v).map_err(write_err),
Const::String(v) => self.string(v),
Const::Class(v) => self.class(v),
Const::MethodType(v) => self.method_type(v),
Const::MethodHandle {
kind,
owner,
name,
descriptor,
itf,
} => self.method_handle(*kind, owner, name, descriptor, *itf),
Const::Dynamic { .. } => Err(ClassfileError::Write(
"CONSTANT_Dynamic пишется только через BootstrapMethods".into(),
)),
}
}
fn string(&mut self, value: &str) -> Result<u16, ClassfileError> {
if let Some(&index) = self.strings.get(value) {
return Ok(index);
}
let string_index = self.utf8(value)?;
let index = self
.pool
.add(Constant::String(string_index))
.map_err(write_err)?;
self.strings.insert(value.to_string(), index);
Ok(index)
}
fn method_type(&mut self, descriptor: &str) -> Result<u16, ClassfileError> {
if let Some(&index) = self.method_types.get(descriptor) {
return Ok(index);
}
let descriptor_index = self.utf8(descriptor)?;
let index = self
.pool
.add(Constant::MethodType(descriptor_index))
.map_err(write_err)?;
self.method_types.insert(descriptor.to_string(), index);
Ok(index)
}
pub(super) fn finish(self) -> ConstantPool<'static> {
self.pool
}
}
fn write_err(error: ristretto_classfile::Error) -> ClassfileError {
ClassfileError::Write(error.to_string())
}

View file

@ -0,0 +1,162 @@
//! Референсы constant pool: `Fieldref`/`Methodref`/`MethodHandle`/`Dynamic`.
//!
//! Это `impl Pool` из [`super`]; вынесено отдельно, чтобы материнский файл
//! держался в лимите структуры. Каждый билдер мемоизирован по семантическому
//! ключу (owner/name/descriptor/…) — одинаковая ссылка всегда получает один
//! индекс, а пул не раздувается дублями (раздувание иначе расширяет `ldc`
//! в `ldc_w` и меняет длину кода при переносе).
use ristretto_classfile::{Constant, ReferenceKind};
use crate::error::ClassfileError;
use crate::insn::InvokeKind;
use super::{Pool, write_err};
impl Pool {
pub(crate) fn field_ref(
&mut self,
owner: &str,
name: &str,
descriptor: &str,
) -> Result<u16, ClassfileError> {
let key = (owner.to_string(), name.to_string(), descriptor.to_string());
if let Some(&index) = self.field_refs.get(&key) {
return Ok(index);
}
let class_index = self.class(owner)?;
let name_and_type_index = self.name_and_type(name, descriptor)?;
let index = self
.pool
.add(Constant::FieldRef {
class_index,
name_and_type_index,
})
.map_err(write_err)?;
self.field_refs.insert(key, index);
Ok(index)
}
pub(crate) fn method_ref(
&mut self,
owner: &str,
name: &str,
descriptor: &str,
kind: InvokeKind,
) -> Result<u16, ClassfileError> {
let itf = matches!(kind, InvokeKind::Interface);
let key = (
owner.to_string(),
name.to_string(),
descriptor.to_string(),
itf,
);
if let Some(&index) = self.method_refs.get(&key) {
return Ok(index);
}
let class_index = self.class(owner)?;
let name_and_type_index = self.name_and_type(name, descriptor)?;
let constant = if itf {
Constant::InterfaceMethodRef {
class_index,
name_and_type_index,
}
} else {
Constant::MethodRef {
class_index,
name_and_type_index,
}
};
let index = self.pool.add(constant).map_err(write_err)?;
self.method_refs.insert(key, index);
Ok(index)
}
pub(crate) fn method_handle(
&mut self,
kind: u8,
owner: &str,
name: &str,
descriptor: &str,
itf: bool,
) -> Result<u16, ClassfileError> {
let key = (
kind,
owner.to_string(),
name.to_string(),
descriptor.to_string(),
itf,
);
if let Some(&index) = self.handles.get(&key) {
return Ok(index);
}
let reference_kind = ReferenceKind::try_from(kind).map_err(|e| {
ClassfileError::Write(format!("некорректный reference_kind {kind}: {e}"))
})?;
let reference = if itf {
self.method_ref(owner, name, descriptor, InvokeKind::Interface)?
} else if matches!(
reference_kind,
ReferenceKind::GetField
| ReferenceKind::GetStatic
| ReferenceKind::PutField
| ReferenceKind::PutStatic
) {
self.field_ref(owner, name, descriptor)?
} else {
self.method_ref(owner, name, descriptor, InvokeKind::Static)?
};
let index = self
.pool
.add_method_handle(reference_kind, reference)
.map_err(write_err)?;
self.handles.insert(key, index);
Ok(index)
}
pub(crate) fn invoke_dynamic(
&mut self,
bootstrap: u16,
name: &str,
descriptor: &str,
) -> Result<u16, ClassfileError> {
self.dynamic_like(bootstrap, name, descriptor, true)
}
pub(crate) fn dynamic(
&mut self,
bootstrap: u16,
name: &str,
descriptor: &str,
) -> Result<u16, ClassfileError> {
self.dynamic_like(bootstrap, name, descriptor, false)
}
fn dynamic_like(
&mut self,
bootstrap: u16,
name: &str,
descriptor: &str,
invoke: bool,
) -> Result<u16, ClassfileError> {
let key = (bootstrap, name.to_string(), descriptor.to_string(), invoke);
if let Some(&index) = self.dynamics.get(&key) {
return Ok(index);
}
let name_and_type_index = self.name_and_type(name, descriptor)?;
let constant = if invoke {
Constant::InvokeDynamic {
bootstrap_method_attr_index: bootstrap,
name_and_type_index,
}
} else {
Constant::Dynamic {
bootstrap_method_attr_index: bootstrap,
name_and_type_index,
}
};
let index = self.pool.add(constant).map_err(write_err)?;
self.dynamics.insert(key, index);
Ok(index)
}
}

View file

@ -0,0 +1,47 @@
//! Инвариант писателя: записанный пул не содержит дублей `CONSTANT_Utf8`.
//!
//! Backend не дедуплицирует (`add_*` всегда толкает новый элемент), а HotSpot
//! сопоставляет запись `LocalVariableTypeTable` с `LocalVariableTable` по
//! сырому `name_index`. Дублирующий `Utf8` для одного имени даёт разные индексы
//! и рушит класс: `ClassFormatError: LVTT entry ... does not match any LVT
//! entry`. Писатель обязан интернировать строки — поэтому в записанном пуле
//! каждый `CONSTANT_Utf8` уникален.
#![allow(clippy::expect_used, clippy::unwrap_used)]
use std::collections::HashSet;
use std::path::PathBuf;
use noxis_classfile::format;
use ristretto_classfile::{ClassFile as RtClass, Constant};
#[test]
fn written_pool_has_no_duplicate_utf8() {
let dir = PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("tests/data");
let mut names: Vec<PathBuf> = std::fs::read_dir(&dir)
.expect("tests/data")
.filter_map(|entry| entry.ok().map(|entry| entry.path()))
.filter(|path| path.extension().is_some_and(|ext| ext == "class"))
.collect();
names.sort();
assert!(!names.is_empty(), "нет фикстур в tests/data");
for path in &names {
let parsed = format::parse(&std::fs::read(path).expect("read")).expect("parse");
let written = format::write(&parsed).expect("write");
let raw = RtClass::from_bytes(&written).expect("written parses");
raw.verify().expect("written verifies");
let mut seen = HashSet::new();
for index in 1..=raw.constant_pool.len() {
if let Some(Constant::Utf8(value)) = raw.constant_pool.get(index as u16) {
let bytes = value.as_bytes().to_vec();
assert!(
seen.insert(bytes),
"дубль CONSTANT_Utf8 {:?} в {}",
String::from_utf8_lossy(value.as_bytes()),
parsed.this_class
);
}
}
}
}

View file

@ -17,6 +17,9 @@ pub enum NameStyle {
MixedLatin, MixedLatin,
/// Псевдосмысленные слова из другого проекта (дезориентация). /// Псевдосмысленные слова из другого проекта (дезориентация).
DecoyWords, DecoyWords,
/// Правдоподобные имена: `TextureCache`, `resolveToken` — вместо
/// явного мусора маскируется под обычный прикладной код.
Plausible,
} }
/// Настройки переименования. /// Настройки переименования.

View file

@ -117,3 +117,40 @@ pub(super) const DECOY_RIGHT: &[&str] = &[
"mirror", "lantern", "harbor", "ember", "shadow", "quarry", "meadow", "cipher", "anchor", "mirror", "lantern", "harbor", "ember", "shadow", "quarry", "meadow", "cipher", "anchor",
"ripple", "ripple",
]; ];
/// Стиль `Plausible`: домен-нейтральные существительные для классов/полей —
/// похожи на обычный прикладной код, не привязаны к конкретному проекту.
pub(super) const PLAUSIBLE_NOUN: &[&str] = &[
"Texture", "Event", "State", "Render", "Asset", "Config", "Session", "Cache", "Buffer",
"Socket", "Stream", "Widget", "Layout", "Theme", "Profile", "Metric", "Token", "Handle",
"Context", "Registry", "Request", "Response", "Payload", "Resource", "Snapshot", "Pipeline",
"Queue", "Slot", "Frame", "Entry",
];
/// Суффиксы ролей для классов (`NounSuffix`, напр. `TextureCache`).
pub(super) const PLAUSIBLE_SUFFIX: &[&str] = &[
"Manager",
"Handler",
"Service",
"Provider",
"Factory",
"Builder",
"Cache",
"Store",
"Loader",
"Parser",
"Worker",
"Controller",
"Adapter",
"Wrapper",
"Registry",
"Resolver",
"Validator",
"Processor",
"Context",
"Listener",
];
/// Глаголы для методов/полей (`verbNoun`, напр. `resolveToken`).
pub(super) const PLAUSIBLE_VERB: &[&str] = &[
"get", "set", "update", "process", "handle", "compute", "resolve", "load", "build", "parse",
"fetch", "apply", "validate", "clear", "reset", "sync", "flush", "bind", "attach", "detach",
];

View file

@ -1,6 +1,8 @@
//! Генерация имён: стили, анти-ключевые слова, уникальность в scope. //! Генерация имён: стили, анти-ключевые слова, уникальность в scope.
pub mod alphabet; pub mod alphabet;
#[cfg(test)]
mod tests;
use std::collections::HashSet; use std::collections::HashSet;
@ -8,7 +10,7 @@ use noxis_core::{NameStyle, Rng};
use self::alphabet::{ use self::alphabet::{
AVOID_CLASS, AVOID_MEMBER, CONFUSABLE, DECOY_LEFT, DECOY_RIGHT, KEYWORDS, LATIN_LOWER, AVOID_CLASS, AVOID_MEMBER, CONFUSABLE, DECOY_LEFT, DECOY_RIGHT, KEYWORDS, LATIN_LOWER,
LATIN_UPPER, UNICODE_LETTERS, LATIN_UPPER, PLAUSIBLE_NOUN, PLAUSIBLE_SUFFIX, PLAUSIBLE_VERB, UNICODE_LETTERS,
}; };
/// Генератор имён с гарантированной уникальностью в пределах одного scope. /// Генератор имён с гарантированной уникальностью в пределах одного scope.
@ -68,7 +70,7 @@ impl NameGenerator {
match self.style { match self.style {
NameStyle::ShortLatin | NameStyle::IlO0 | NameStyle::MixedLatin => "z", NameStyle::ShortLatin | NameStyle::IlO0 | NameStyle::MixedLatin => "z",
NameStyle::Unicode => "\u{043e}", NameStyle::Unicode => "\u{043e}",
NameStyle::DecoyWords => "tmp", NameStyle::DecoyWords | NameStyle::Plausible => "tmp",
} }
} }
@ -121,6 +123,20 @@ impl NameGenerator {
} }
name name
} }
// Маскируется под обычный прикладной код, а не под мусор:
// классы — `NounSuffix` (`TextureCache`), члены — `verbNoun`
// (`resolveToken`) camelCase.
NameStyle::Plausible => {
let noun = PLAUSIBLE_NOUN[self.rng.below(PLAUSIBLE_NOUN.len() as u64) as usize];
if self.avoid_class {
let suffix =
PLAUSIBLE_SUFFIX[self.rng.below(PLAUSIBLE_SUFFIX.len() as u64) as usize];
format!("{noun}{suffix}")
} else {
let verb = PLAUSIBLE_VERB[self.rng.below(PLAUSIBLE_VERB.len() as u64) as usize];
format!("{verb}{noun}")
}
}
} }
} }
@ -164,52 +180,3 @@ fn capitalize(s: &str) -> String {
None => String::new(), None => String::new(),
} }
} }
#[cfg(test)]
mod tests {
#![allow(clippy::expect_used, clippy::unwrap_used)]
use super::alphabet::{AVOID_MEMBER, KEYWORDS};
use super::*;
#[test]
fn names_are_unique_and_valid() {
let mut names = NameGenerator::new(Rng::from_seed(1), NameStyle::ShortLatin);
let mut seen = HashSet::new();
for _ in 0..500 {
let name = names.next_name();
assert!(seen.insert(name.clone()), "повтор имени {name}");
assert!(!KEYWORDS.contains(&name.as_str()));
assert!(name.starts_with(|c: char| c.is_ascii_alphabetic()));
}
}
#[test]
fn avoids_reserved_member_names() {
let mut names = NameGenerator::new(Rng::from_seed(2), NameStyle::DecoyWords);
for _ in 0..500 {
let name = names.next_name();
assert!(!AVOID_MEMBER.contains(&name.as_str()));
}
}
#[test]
fn unicode_style_produces_identifier_valid_names() {
let mut names = NameGenerator::new(Rng::from_seed(3), NameStyle::Unicode);
for _ in 0..100 {
let name = names.next_name();
assert!(
name.chars()
.all(|c| c.is_alphanumeric() || c == '_' || c == '$')
);
}
}
#[test]
fn reserved_names_never_reissued() {
let mut names = NameGenerator::new(Rng::from_seed(4), NameStyle::ShortLatin);
names.reserve("abc");
for _ in 0..300 {
assert_ne!(names.next_name(), "abc");
}
}
}

View file

@ -0,0 +1,76 @@
//! Тесты генератора имён: уникальность, валидность, форма по стилю.
#![allow(clippy::expect_used, clippy::unwrap_used)]
use std::collections::HashSet;
use noxis_core::{NameStyle, Rng};
use super::NameGenerator;
use super::alphabet::{AVOID_MEMBER, KEYWORDS};
#[test]
fn names_are_unique_and_valid() {
let mut names = NameGenerator::new(Rng::from_seed(1), NameStyle::ShortLatin);
let mut seen = HashSet::new();
for _ in 0..500 {
let name = names.next_name();
assert!(seen.insert(name.clone()), "повтор имени {name}");
assert!(!KEYWORDS.contains(&name.as_str()));
assert!(name.starts_with(|c: char| c.is_ascii_alphabetic()));
}
}
#[test]
fn avoids_reserved_member_names() {
let mut names = NameGenerator::new(Rng::from_seed(2), NameStyle::DecoyWords);
for _ in 0..500 {
let name = names.next_name();
assert!(!AVOID_MEMBER.contains(&name.as_str()));
}
}
#[test]
fn unicode_style_produces_identifier_valid_names() {
let mut names = NameGenerator::new(Rng::from_seed(3), NameStyle::Unicode);
for _ in 0..100 {
let name = names.next_name();
assert!(
name.chars()
.all(|c| c.is_alphanumeric() || c == '_' || c == '$')
);
}
}
#[test]
fn reserved_names_never_reissued() {
let mut names = NameGenerator::new(Rng::from_seed(4), NameStyle::ShortLatin);
names.reserve("abc");
for _ in 0..300 {
assert_ne!(names.next_name(), "abc");
}
}
#[test]
fn plausible_class_names_look_like_noun_suffix() {
let mut names = NameGenerator::new(Rng::from_seed(5), NameStyle::Plausible).for_classes();
for _ in 0..50 {
let name = names.next_name();
assert!(name.starts_with(|c: char| c.is_ascii_uppercase()), "{name}");
// Два слова подряд слиплись в CamelCase без разделителей.
assert!(
name.chars().filter(|c| c.is_ascii_uppercase()).count() >= 2,
"{name}"
);
}
}
#[test]
fn plausible_member_names_look_like_verb_noun() {
let mut names = NameGenerator::new(Rng::from_seed(6), NameStyle::Plausible);
for _ in 0..50 {
let name = names.next_name();
assert!(name.starts_with(|c: char| c.is_ascii_lowercase()), "{name}");
assert!(!KEYWORDS.contains(&name.as_str()));
}
}

View file

@ -180,6 +180,7 @@ pub(super) fn generate_packages(cfg: &Config, rng: &mut Rng) -> Vec<String> {
let style = match cfg.rename.class_style { let style = match cfg.rename.class_style {
NameStyle::Unicode => NameStyle::Unicode, NameStyle::Unicode => NameStyle::Unicode,
NameStyle::IlO0 => NameStyle::IlO0, NameStyle::IlO0 => NameStyle::IlO0,
NameStyle::Plausible => NameStyle::Plausible,
_ => NameStyle::MixedLatin, _ => NameStyle::MixedLatin,
}; };
let mut names = NameGenerator::new(rng.clone(), style).for_classes(); let mut names = NameGenerator::new(rng.clone(), style).for_classes();

View file

@ -27,9 +27,19 @@
`ClassNotFoundException` в рантайме. `ClassNotFoundException` в рантайме.
- Рефлексия через строки мы ловим автоматически (анализ `Class.forName`, - Рефлексия через строки мы ловим автоматически (анализ `Class.forName`,
`getMethod`/`getField` и т.п.), но динамическую конкатенацию — нет. `getMethod`/`getField` и т.п.), но динамическую конкатенацию — нет.
- JSON-(де)сериализация по именам полей (Gson/Jackson/`Codec`) не ловится:
биндинг идёт через `X.class`-литерал, не строку — см. `todo.md` §2.
- Для библиотек `rename_public_members = false` по умолчанию: менять - Для библиотек `rename_public_members = false` по умолчанию: менять
публичный API библиотеки — сломать всех потребителей. публичный API библиотеки — сломать всех потребителей.
Шесть стилей имён (`ShortLatin`, `IlO0`, `Unicode`, `MixedLatin`,
`DecoyWords`, `Plausible`) — компромисс по-разному: `IlO0`/`Unicode` явно
сигнализируют «это обфускация», `Plausible` маскируется под обычный
прикладной код (`TextureCache`, `resolveToken`), ценой того, что при
ручном чтении он менее заметно отличается от настоящей логики, чем
намеренный мусор — скрыть обфускацию и скрыть её цену ценой чуть большей
путаницы для собственной команды при отладке маппингов.
**Статус: готово.** **Статус: готово.**
## 2. Шифрование строк (`Strings`) ## 2. Шифрование строк (`Strings`)