frames,jar,passes: verifier-driven hardening from real jars

Proven by paranoid runs over lorep (17 classes, fully green),
LoVisual (4081 classes) and oc2r (1278 classes) with per-class
java -Xverify:all, each fix first reproduced as a red test:

- merge: Top absorbs instead of Top-wins (javac declares Top for
  slots unwritten on some inflow; precise types reject those edges).
  Regression: loop_head_keeps_top_for_loop_local.
- emit: collapse [Long|Double, Top] pairs on operand stacks
  (javac writes one entry; the pair is rejected). Regression:
  wide_stack_join_emits_single_entries.
- xfer ArrayLoad: push the wide pair, fixing lcmp underflow.
  Regression: array_load_wide_pushes_pair.
- xfer <init>: convert only the receiver's own allocation site
  instead of every Uninitialized (nested construction clobbered
  the outer object). Regression: nested_construction_keeps_outer.
- xfer ANewArray: array operands gain one dimension ([B] -> [[B)
  instead of an illegal L-wrap ([L[B;). Covered by the same test.
- jar load: duplicate class names (multi-release module-info
  twins) keep the first occurrence instead of crashing save.
  Test: duplicate_class_names_keep_first.
- strings/constants: skip methods past MAX_METHOD_INSNS (8000)
  so giants cannot overflow the 64KB writer ceiling. Tests:
  oversized_methods_are_skipped in both passes.
- merge errors now name both types being merged.

Also: em-dash removed repo-wide (724 spots, rule added to
todo.md), allow-lints only in tests (scratch example deleted),
code comments migrated to English in every file this change
touches.
This commit is contained in:
Loki 2026-10-09 13:47:27 +02:00
parent 2172206e83
commit 139aea9a8f
Signed by: boba
GPG key ID: 253067914055423B
96 changed files with 1053 additions and 831 deletions

View file

@ -1,9 +1,11 @@
//! Перевод архива в [`Program`] и обратно.
//! Archive to [`Program`] and back.
//!
//! Правило простое: `*.class` уходит в парсер, всё остальное — в ресурсы.
//! Обратно классы сериализуются первыми, затем ресурсы: порядок внутри
//! архива на поведение не влияет, а фиксированный порядок даёт
//! воспроизводимую запись.
//! The rule is simple: `*.class` goes to the parser, everything else -
//! to resources. Classes serialize first, then resources: the order inside
//! the archive does not affect behaviour, and a fixed order gives
//! reproducible writes.
use std::collections::BTreeSet;
use noxis_classfile::Program;
use noxis_classfile::format;
@ -11,13 +13,22 @@ use noxis_classfile::model::Resource;
use crate::archive::{Entry, JarError};
/// Разбирает записи архива: классы — через парсер, остальное — как ресурсы.
/// Parses archive entries: classes - through the parser, the rest - as resources.
///
/// A name that repeats (multi-release jars carry the same class at the root
/// and under `META-INF/versions/*`) is kept once, first occurrence wins:
/// the flat model cannot hold two classes under one name, and later passes
/// (rename maps, jar writing) assume uniqueness.
pub fn load(entries: Vec<Entry>) -> Result<Program, JarError> {
let mut program = Program::default();
let mut seen: BTreeSet<String> = BTreeSet::new();
for entry in entries {
if is_class_entry(&entry.name) {
program.classes.push(format::parse(&entry.data)?);
let class = format::parse(&entry.data)?;
if seen.insert(class.this_class.clone()) {
program.classes.push(class);
}
} else {
program.resources.push(Resource {
path: entry.name,
@ -29,7 +40,7 @@ pub fn load(entries: Vec<Entry>) -> Result<Program, JarError> {
Ok(program)
}
/// Собирает записи архива из [`Program`]: сначала классы, потом ресурсы.
/// Assembles archive entries from [`Program`]: classes first, then resources.
pub fn save(program: &Program) -> Result<Vec<Entry>, JarError> {
let mut entries = Vec::with_capacity(program.classes.len() + program.resources.len());
@ -44,7 +55,7 @@ pub fn save(program: &Program) -> Result<Vec<Entry>, JarError> {
Ok(entries)
}
/// Классовый файл внутри архива: путь оканчивается на `.class`.
/// A class file inside the archive: the path ends in `.class`.
fn is_class_entry(name: &str) -> bool {
name.len() > ".class".len() && name.ends_with(".class")
}
@ -78,7 +89,7 @@ mod tests {
assert_eq!(save(&program).expect("save"), source);
}
/// До P1 `format::parse` — заглушка; после P1 ожидается успешный разбор.
/// Class entries go through the parser (round-trip keeps them intact).
#[test]
fn class_entries_go_through_the_parser() {
let entries = vec![
@ -97,4 +108,22 @@ mod tests {
Err(other) => panic!("unexpected error: {other}"),
}
}
/// Multi-release twins (`demo/App.class` + `META-INF/versions/9/...`):
/// the flat model keeps the first occurrence so no two classes ever
/// share one name downstream.
#[test]
fn duplicate_class_names_keep_first() {
use noxis_classfile::ClassBuilder;
let class = ClassBuilder::new("demo/App", None).build();
let bytes = format::write(&class).expect("write");
let entries = vec![
Entry::new("demo/App.class", bytes.clone()),
Entry::new("META-INF/versions/9/demo/App.class", bytes),
];
let program = load(entries).expect("load");
assert_eq!(program.classes.len(), 1);
assert_eq!(program.classes[0].this_class, "demo/App");
}
}