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:
parent
a2e4c69210
commit
f08bf60635
10 changed files with 509 additions and 206 deletions
|
|
@ -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())
|
|
||||||
}
|
|
||||||
155
crates/noxis-classfile/src/format/write/pool/mod.rs
Normal file
155
crates/noxis-classfile/src/format/write/pool/mod.rs
Normal 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())
|
||||||
|
}
|
||||||
162
crates/noxis-classfile/src/format/write/pool/refs.rs
Normal file
162
crates/noxis-classfile/src/format/write/pool/refs.rs
Normal 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)
|
||||||
|
}
|
||||||
|
}
|
||||||
47
crates/noxis-classfile/tests/pool/main.rs
Normal file
47
crates/noxis-classfile/tests/pool/main.rs
Normal 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
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -17,6 +17,9 @@ pub enum NameStyle {
|
||||||
MixedLatin,
|
MixedLatin,
|
||||||
/// Псевдосмысленные слова из другого проекта (дезориентация).
|
/// Псевдосмысленные слова из другого проекта (дезориентация).
|
||||||
DecoyWords,
|
DecoyWords,
|
||||||
|
/// Правдоподобные имена: `TextureCache`, `resolveToken` — вместо
|
||||||
|
/// явного мусора маскируется под обычный прикладной код.
|
||||||
|
Plausible,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Настройки переименования.
|
/// Настройки переименования.
|
||||||
|
|
|
||||||
|
|
@ -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",
|
||||||
|
];
|
||||||
|
|
|
||||||
|
|
@ -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");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
|
||||||
76
crates/noxis-passes/src/namegen/tests.rs
Normal file
76
crates/noxis-passes/src/namegen/tests.rs
Normal 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()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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();
|
||||||
|
|
|
||||||
|
|
@ -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`)
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue