From f08bf60635bee42e7681891995e560416f0657bb Mon Sep 17 00:00:00 2001 From: loki5512344 Date: Thu, 8 Oct 2026 22:55:49 +0200 Subject: [PATCH] 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. --- .../noxis-classfile/src/format/write/pool.rs | 155 ----------------- .../src/format/write/pool/mod.rs | 155 +++++++++++++++++ .../src/format/write/pool/refs.rs | 162 ++++++++++++++++++ crates/noxis-classfile/tests/pool/main.rs | 47 +++++ crates/noxis-core/src/config/sections.rs | 3 + crates/noxis-passes/src/namegen/alphabet.rs | 37 ++++ crates/noxis-passes/src/namegen/mod.rs | 69 ++------ crates/noxis-passes/src/namegen/tests.rs | 76 ++++++++ crates/noxis-passes/src/rename/assign.rs | 1 + docs/techniques.md | 10 ++ 10 files changed, 509 insertions(+), 206 deletions(-) delete mode 100644 crates/noxis-classfile/src/format/write/pool.rs create mode 100644 crates/noxis-classfile/src/format/write/pool/mod.rs create mode 100644 crates/noxis-classfile/src/format/write/pool/refs.rs create mode 100644 crates/noxis-classfile/tests/pool/main.rs create mode 100644 crates/noxis-passes/src/namegen/tests.rs diff --git a/crates/noxis-classfile/src/format/write/pool.rs b/crates/noxis-classfile/src/format/write/pool.rs deleted file mode 100644 index cee7efc..0000000 --- a/crates/noxis-classfile/src/format/write/pool.rs +++ /dev/null @@ -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 { - self.pool.add_utf8(value).map_err(write_err) - } - - pub(super) fn name_and_type( - &mut self, - name: &str, - descriptor: &str, - ) -> Result { - self.pool - .add_name_and_type(name, descriptor) - .map_err(write_err) - } - - pub(super) fn class(&mut self, name: &str) -> Result { - self.pool.add_class(name).map_err(write_err) - } - - pub(super) fn field_ref( - &mut self, - owner: &str, - name: &str, - descriptor: &str, - ) -> Result { - 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 { - 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 { - 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 { - 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 { - 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 { - 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()) -} diff --git a/crates/noxis-classfile/src/format/write/pool/mod.rs b/crates/noxis-classfile/src/format/write/pool/mod.rs new file mode 100644 index 0000000..27cccc1 --- /dev/null +++ b/crates/noxis-classfile/src/format/write/pool/mod.rs @@ -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, + pub(super) classes: HashMap, + pub(super) name_types: HashMap<(String, String), u16>, + pub(super) strings: HashMap, + pub(super) method_types: HashMap, + 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 { + 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 { + 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 { + 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 { + 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 { + 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 { + 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()) +} diff --git a/crates/noxis-classfile/src/format/write/pool/refs.rs b/crates/noxis-classfile/src/format/write/pool/refs.rs new file mode 100644 index 0000000..67e6e02 --- /dev/null +++ b/crates/noxis-classfile/src/format/write/pool/refs.rs @@ -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 { + 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 { + 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 { + 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 { + self.dynamic_like(bootstrap, name, descriptor, true) + } + + pub(crate) fn dynamic( + &mut self, + bootstrap: u16, + name: &str, + descriptor: &str, + ) -> Result { + self.dynamic_like(bootstrap, name, descriptor, false) + } + + fn dynamic_like( + &mut self, + bootstrap: u16, + name: &str, + descriptor: &str, + invoke: bool, + ) -> Result { + 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) + } +} diff --git a/crates/noxis-classfile/tests/pool/main.rs b/crates/noxis-classfile/tests/pool/main.rs new file mode 100644 index 0000000..aef55f1 --- /dev/null +++ b/crates/noxis-classfile/tests/pool/main.rs @@ -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 = 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 + ); + } + } + } +} diff --git a/crates/noxis-core/src/config/sections.rs b/crates/noxis-core/src/config/sections.rs index a35141e..54f56f2 100644 --- a/crates/noxis-core/src/config/sections.rs +++ b/crates/noxis-core/src/config/sections.rs @@ -17,6 +17,9 @@ pub enum NameStyle { MixedLatin, /// Псевдосмысленные слова из другого проекта (дезориентация). DecoyWords, + /// Правдоподобные имена: `TextureCache`, `resolveToken` — вместо + /// явного мусора маскируется под обычный прикладной код. + Plausible, } /// Настройки переименования. diff --git a/crates/noxis-passes/src/namegen/alphabet.rs b/crates/noxis-passes/src/namegen/alphabet.rs index 609ec8a..cf4c524 100644 --- a/crates/noxis-passes/src/namegen/alphabet.rs +++ b/crates/noxis-passes/src/namegen/alphabet.rs @@ -117,3 +117,40 @@ pub(super) const DECOY_RIGHT: &[&str] = &[ "mirror", "lantern", "harbor", "ember", "shadow", "quarry", "meadow", "cipher", "anchor", "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", +]; diff --git a/crates/noxis-passes/src/namegen/mod.rs b/crates/noxis-passes/src/namegen/mod.rs index 6d09b1d..2efdf64 100644 --- a/crates/noxis-passes/src/namegen/mod.rs +++ b/crates/noxis-passes/src/namegen/mod.rs @@ -1,6 +1,8 @@ //! Генерация имён: стили, анти-ключевые слова, уникальность в scope. pub mod alphabet; +#[cfg(test)] +mod tests; use std::collections::HashSet; @@ -8,7 +10,7 @@ use noxis_core::{NameStyle, Rng}; use self::alphabet::{ 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. @@ -68,7 +70,7 @@ impl NameGenerator { match self.style { NameStyle::ShortLatin | NameStyle::IlO0 | NameStyle::MixedLatin => "z", NameStyle::Unicode => "\u{043e}", - NameStyle::DecoyWords => "tmp", + NameStyle::DecoyWords | NameStyle::Plausible => "tmp", } } @@ -121,6 +123,20 @@ impl NameGenerator { } 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(), } } - -#[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"); - } - } -} diff --git a/crates/noxis-passes/src/namegen/tests.rs b/crates/noxis-passes/src/namegen/tests.rs new file mode 100644 index 0000000..3fa0cb6 --- /dev/null +++ b/crates/noxis-passes/src/namegen/tests.rs @@ -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())); + } +} diff --git a/crates/noxis-passes/src/rename/assign.rs b/crates/noxis-passes/src/rename/assign.rs index 3d68353..2838b38 100644 --- a/crates/noxis-passes/src/rename/assign.rs +++ b/crates/noxis-passes/src/rename/assign.rs @@ -180,6 +180,7 @@ pub(super) fn generate_packages(cfg: &Config, rng: &mut Rng) -> Vec { let style = match cfg.rename.class_style { NameStyle::Unicode => NameStyle::Unicode, NameStyle::IlO0 => NameStyle::IlO0, + NameStyle::Plausible => NameStyle::Plausible, _ => NameStyle::MixedLatin, }; let mut names = NameGenerator::new(rng.clone(), style).for_classes(); diff --git a/docs/techniques.md b/docs/techniques.md index dfde703..ac1f2f6 100644 --- a/docs/techniques.md +++ b/docs/techniques.md @@ -27,9 +27,19 @@ `ClassNotFoundException` в рантайме. - Рефлексия через строки мы ловим автоматически (анализ `Class.forName`, `getMethod`/`getField` и т.п.), но динамическую конкатенацию — нет. +- JSON-(де)сериализация по именам полей (Gson/Jackson/`Codec`) не ловится: + биндинг идёт через `X.class`-литерал, не строку — см. `todo.md` §2. - Для библиотек `rename_public_members = false` по умолчанию: менять публичный API библиотеки — сломать всех потребителей. +Шесть стилей имён (`ShortLatin`, `IlO0`, `Unicode`, `MixedLatin`, +`DecoyWords`, `Plausible`) — компромисс по-разному: `IlO0`/`Unicode` явно +сигнализируют «это обфускация», `Plausible` маскируется под обычный +прикладной код (`TextureCache`, `resolveToken`), ценой того, что при +ручном чтении он менее заметно отличается от настоящей логики, чем +намеренный мусор — скрыть обфускацию и скрыть её цену ценой чуть большей +путаницы для собственной команды при отладке маппингов. + **Статус: готово.** ## 2. Шифрование строк (`Strings`)