Skip to main content

ddc_dec/
classdec.rs

1//! Class-level rendering: header, fields (with static initializers),
2//! constructors, methods and nested member classes.
3
4use jdc_core::emit::{escape_string, format_float, Printer};
5use jdc_core::types::JavaType;
6use jdc_core::Ctx;
7
8use crate::access::*;
9use crate::PoolField;
10use crate::ctx::{java_type_to_generic, DexCtx};
11use crate::method::decompile_method;
12use crate::{desc_type, DexPool, PoolClass, PoolMethod, StaticValue};
13use ddc_dex::insn::InsnKind;
14
15#[derive(Debug, Clone)]
16pub struct ClassOptions {
17    /// Prefix each file with a provenance comment.
18    pub provenance: bool,
19}
20
21impl Default for ClassOptions {
22    fn default() -> Self {
23        ClassOptions { provenance: true }
24    }
25}
26
27/// Decompile one class (plus nested member classes) to a Java source file.
28///
29/// Classes with large method bodies run in a monitored thread with a
30/// deadline: pathological CFGs can drive the shared structurer's walk into
31/// an exponential exploration that never returns. On timeout the class is
32/// reported failed and the thread is abandoned (reaped at process exit).
33/// One registered monitored decompile: the receiver the worker polls at
34/// the tail, the class name (for diagnostics), and the deadline counted
35/// from the spawn.
36pub type PendingMonitor = (
37    std::sync::mpsc::Receiver<Result<String, String>>,
38    String,
39    std::time::Instant,
40);
41
42pub fn decompile_class(
43    pool: &std::sync::Arc<DexPool>,
44    class: &PoolClass,
45    opts: &ClassOptions,
46    pending: &std::sync::Mutex<Vec<PendingMonitor>>,
47) -> anyhow::Result<String> {
48    if class_is_risky(pool, class) {
49        // Detached monitored thread: the CALLER registers the receiver and
50        // moves on (awaiting happens at the end of the run) — a spinning
51        // pathological method no longer stalls its worker.
52        let pool2 = pool.clone();
53        let cls = class.clone();
54        let opts2 = opts.clone();
55        let (tx, rx) = std::sync::mpsc::channel::<Result<String, String>>();
56        let name = class.name.clone();
57        // 10s: pure insurance — the structurer's walk budgets and the
58        // ladder giant short-circuit bound pathological methods
59        // deterministically now (weixin cdp/l1: 4.2s single-class), but
60        // full-run worker contention can ~2× that; the old 5s dropped
61        // l1's file entirely (every referrer cannot-finds).
62        let deadline = std::time::Instant::now() + std::time::Duration::from_secs(10);
63        let _ = std::thread::Builder::new()
64            .stack_size(64 * 1024 * 1024)
65            .spawn(move || {
66                let r = decompile_class_impl(&pool2, &cls, &opts2).map_err(|e| format!("{:#}", e));
67                let _ = tx.send(r);
68            });
69        pending.lock().unwrap().push((rx, name, deadline));
70        return Err(anyhow::anyhow!(
71            "deferred to monitored thread (pathological-CFG guard)"
72        ));
73    }
74    decompile_class_impl(pool, class, opts)
75}
76
77/// Any method in the exponential-walk danger zone → run under the
78/// deadline. Calibrated between real workloads: routine R8 classes top out
79/// near 11k insns (reqable's biggest `<clinit>`); the exponential hangs
80/// (weibo's gson TypeAdapters) sit at 24k+.
81fn class_is_risky(pool: &DexPool, class: &PoolClass) -> bool {
82    // Header peek only — decoding every body here would double the APK's
83    // total decode work.
84    for m in class.all_methods() {
85        if m.code_off == 0 {
86            continue;
87        }
88        if let Some(dex) = pool.dex(m.dex_idx) {
89            if let Some((_regs, insns)) = dex.code_stats(m.code_off) {
90                if insns > 16_000 {
91                    return true;
92                }
93            }
94        }
95    }
96    false
97}
98
99fn decompile_class_impl(
100    pool: &DexPool,
101    class: &PoolClass,
102    opts: &ClassOptions,
103) -> anyhow::Result<String> {
104    let ctx = DexCtx::new(pool, class);
105    // Size the class buffer up front: corpus-average classes render to
106    // ~1KB per method — without the reserve the String doubles through
107    // 4-6 realloc+copy rounds per class (~2× the final size in memmove).
108    // Cap the reserve: weixin's monster classes (hundreds of methods)
109    // would reserve megabytes of untouched capacity per class — large
110    // blocks take mimalloc's commit/purge path (madvise churn showed up
111    // in profiles). 256KB covers the corpus-average class dozens of
112    // times over; bigger classes pay a few extra doublings.
113    let mut out = String::with_capacity(
114        (class.all_methods().count() * 1024).min(256 * 1024) + 512,
115    );
116    if opts.provenance {
117        static BANNER: std::sync::OnceLock<String> = std::sync::OnceLock::new();
118        out.push_str(BANNER.get_or_init(|| {
119            format!(
120                "// Decompiled by https://github.com/ejfkdev/ddc {}\n",
121                env!("CARGO_PKG_VERSION")
122            )
123        }));
124        // Provenance: which input image this class was lifted from (jadx's
125        // `loaded from: classes.dex` pattern). Byte-stable across runs —
126        // deliberately NO timestamp so outputs diff cleanly. Direct
127        // push_str: the per-class format! temporaries were 3 allocations
128        // × every class in the corpus.
129        if let Some(dex) = pool.dex(class.dex_idx) {
130            if let Some(label) = pool.dex_labels.get(class.dex_idx) {
131                out.push_str("// From: ");
132                out.push_str(label);
133                out.push_str(" (DEX ");
134                out.push_str(&dex.version);
135                out.push_str(")\n");
136            }
137        }
138    }
139    if let Some(src) = &class.source_file {
140        out.push_str("// Source file: ");
141        out.push_str(src);
142        out.push('\n');
143    }
144    if class.is_synthetic() {
145        out.push_str("// synthetic\n");
146    }
147    let (pkg, _) = split_name(&class.name);
148    // Two-pass import assembly: the body renders FIRST (into a temp
149    // buffer) with the obscured render state installed — expression
150    // positions DISCOVER additional obscured refs on the fly — then the
151    // file assembles as provenance + package + imports + body. The one
152    // extra body copy is the price of correct import placement.
153    let mut obscured_map = compute_obscured_renders(pool, class);
154    // Same-package simple-name collisions: an import shadows every
155    // same-package use of that simple in this file — drop those from
156    // the map (their refs stay qualified, erroring honestly).
157    let blocked: jdc_core::FxHashSet<String> = pool
158        .package_simples()
159        .get(&pkg)
160        .cloned()
161        .unwrap_or_default();
162    obscured_map.retain(|_internal, simple| !blocked.contains(simple));
163    set_obscured_state(class.name.clone(), obscured_map.clone(), blocked.clone());
164    let mut body_buf = String::with_capacity(out.capacity() / 2);
165    let body_res = emit_class_body(pool, class, &ctx, opts, &mut body_buf, 0);
166    let recorded = take_recorded_and_clear();
167    body_res?;
168    if !pkg.is_empty() {
169        out.push('\n');
170        out.push_str("package ");
171        let dotted = pkg.replace('/', ".");
172        out.push_str(&sanitize_fq(&dotted));
173        out.push_str(";\n");
174        // Imports: the metadata map plus the expression-level
175        // discoveries, pool-known only (an import of an unknown class
176        // is a hard error), display-renamed, sorted for determinism.
177        let mut import_set: jdc_core::FxHashSet<String> =
178            obscured_map.keys().cloned().collect();
179        for r in recorded {
180            if pool.get(&r).is_some() {
181                let simple = r.rsplit('/').next().unwrap_or("");
182                if !blocked.contains(simple) {
183                    import_set.insert(r);
184                }
185            }
186        }
187        let mut imports: Vec<String> = import_set
188            .iter()
189            .map(|internal| crate::classdec::dotted(&crate::apply_class_rename(internal)))
190            .collect();
191        imports.sort();
192        for display in imports {
193            out.push_str(&format!("import {};\n", display));
194        }
195    }
196    out.push('\n');
197    out.push_str(&body_buf);
198    Ok(out)
199}
200
201/// Render the class header, fields, methods and nested member classes into
202/// `out` (used at top level and recursively for nested members).
203// `opts` is only consumed by the nested-member recursion below — that
204// is its purpose (propagating emission options into inline children).
205/// One enum constant: the source identifier plus constructor
206/// arguments beyond the compiler-mandated `(String name, int ordinal)`.
207use jdc_core::ir::expr::{ConstVal, Expr};
208use jdc_core::ir::stmt::Stmt;
209struct EnumConst {
210    #[allow(dead_code)]
211    field: String,
212    name: String,
213    extra_args: Vec<Expr>,
214}
215
216/// Collect enum constants for a true `enum` rendering. Every ACC_ENUM
217/// static field must be initialized in `<clinit>` by
218/// `Self.field = new Self("NAME", ordinal, ...)` — the shape javac/d8
219/// always emit. Returns None (caller falls back to the desugared
220/// `/* enum */ class` form) when anything is missing: R8 variance,
221/// constant-specific bodies (the field holds an anonymous subclass), or
222/// a <clinit> that failed to decompile.
223fn collect_enum_constants(
224    pool: &DexPool,
225    class: &PoolClass,
226    ctx: &DexCtx<'_>,
227) -> Option<(Vec<EnumConst>, crate::method::MethodBody)> {
228    let _ = ctx;
229    let const_fields: Vec<&PoolField> = class
230        .static_fields
231        .iter()
232        .filter(|f| f.access & crate::access::ACC_ENUM != 0)
233        .collect();
234    // No ACC_ENUM constant fields: R8 drops them ALL when nothing
235    // outside reads them (weixin sp2/ji — only the synthetic $VALUES
236    // array survives). An ACC_ENUM CLASS still promotes: the synthetic-
237    // constant scan below lifts the constants out of the array literal.
238    // Non-enum classes have no business here.
239    if const_fields.is_empty() && class.access & crate::access::ACC_ENUM == 0 {
240        return None;
241    }
242    let clinit = class.all_methods().find(|m| &*m.name == "<clinit>")?;
243    let mut body = decompile_method(pool, class, clinit).ok().flatten()?;
244    // The structurer can leave the constant-build sequence inside a
245    // nested Stmt::Block (jd1.u: stmt4 wraps all 26 ctors beside a
246    // trailing loop) — the passes below index a FLAT statement list.
247    if let Stmt::Block(vs) = &mut body.body {
248        crate::passes::flatten_top_blocks(vs);
249    }
250
251    // R8/d8 split the constant build across an intermediate local:
252    //   Self v0 = new Self("NAME", i, ...);
253    //   a = v0;                       // sput to the ACC_ENUM field
254    //   Self[] v5 = new Self[n]; v5[k] = v0; ...; $VALUES = v5;
255    // Pass 1 registers each intermediate (or direct) new; pass 2 binds
256    // them to the ACC_ENUM fields; references to the locals are then
257    // rewritten into constant identifiers so the $VALUES build keeps
258    // compiling once the definitions drop.
259    use std::collections::HashMap;
260    let mut const_name: Vec<String> = Vec::new();
261    let mut const_field: Vec<String> = Vec::new();
262    let mut const_extra: Vec<Vec<Expr>> = Vec::new();
263    // Declared ordinal per constant (the ctor's int arg) — the merge at
264    // the end orders field-bound and synthetic constants by it and
265    // demands a dense 0..n run (Java derives ordinals from declaration
266    // position).
267    let mut const_ord: Vec<i64> = Vec::new();
268    let mut var_of: HashMap<u32, usize> = HashMap::default(); // local id -> const idx
269    let mut drop_stmts: Vec<usize> = Vec::new();
270
271    // Collect (immutable borrows) first; the mutable passes come after.
272    if !matches!(&body.body, Stmt::Block(_)) { return None; }
273
274    // Pass 1: definitions. (immutable borrow; rewrite comes later)
275    // Rolling reaching-defs of clinit locals for resolving enum-ctor
276    // extra args (resolve_enum_arg); `tainted` flips at the first
277    // non-straight-line statement — past it, linear-scan defs are no
278    // longer provable and Local args reject the enum mode.
279    let mut defs: HashMap<u32, &Expr> = HashMap::default();
280    let mut tainted = false;
281    let self_name: std::sync::Arc<str> = class.name.as_str().into();
282    let self_ty =
283        jdc_core::ir::expr::TypeRef::J(JavaType::Object(class.name.as_str().into()));
284    // A REUSED intermediate local: `v = new Self(..); A = v; v = new
285    // Self(..); B = v;` — the first def is a LocalDef, the rest are
286    // Assigns to the same var. Register every def site; pass 2 resolves
287    // a local sput to the def site CURRENT at that statement (rolling),
288    // not to the var (Kotlin EnumEntries enums, RegexOption: 8→7 count
289    // mismatch used to abort the whole promotion).
290    let mut def_site: HashMap<usize, (u32, usize)> = HashMap::default();
291    for (i, st) in match &body.body {
292        Stmt::Block(v) => v.iter().enumerate(),
293        _ => return None,
294    } {
295        // (var, New) for both def shapes: LocalDef and Assign-to-local.
296        let def_shape: Option<(u32, &Expr)> = match st {
297            Stmt::LocalDef {
298                var,
299                init: Some(e),
300                ..
301            } => Some((*var, e)),
302            Stmt::ExprStmt(Expr::Assign { target, value, .. }) => {
303                match (&**target, &**value) {
304                    (Expr::Local { var, .. }, e) => Some((*var, e)),
305                    _ => None,
306                }
307            }
308            _ => None,
309        };
310        if let Some((var, Expr::New { cls: ncls, args, .. })) = def_shape {
311            if ncls.as_ref() == class.name && args.len() >= 2 {
312                if let (Expr::Const(ConstVal::Str(n)), Expr::Const(ConstVal::Int(ord0))) =
313                    (&args[0], &args[1])
314                {
315                    if java_ident(n).as_ref() != &**n || n.is_empty() { return None; }
316                    var_of.insert(var, const_name.len());
317                    def_site.insert(i, (var, const_name.len()));
318                    const_ord.push(*ord0 as i64);
319                    const_name.push(n.to_string());
320                    const_field.push(String::new());
321                    const_extra.push(resolve_enum_extras(
322                        &args[2..],
323                        &defs,
324                        tainted,
325                        &var_of,
326                        &const_name,
327                        &self_name,
328                        &self_ty,
329                    )?);
330                    drop_stmts.push(i);
331                }
332            }
333        }
334        track_def(st, &mut defs, &mut tainted);
335    }
336
337    // Pass 2: sputs to the ACC_ENUM fields (direct new or intermediate).
338    // Local sputs resolve through the ROLLING def map (updated at each
339    // def-site statement) — a reused `v` must bind to the constant
340    // defined most recently, not to the var's first registration.
341    let mut defs2: HashMap<u32, &Expr> = HashMap::default();
342    let mut tainted2 = false;
343    let mut cur_def: HashMap<u32, usize> = HashMap::default();
344    for (i, st) in match &body.body {
345        Stmt::Block(v) => v.iter().enumerate(),
346        _ => return None,
347    } {
348        if let Some((var, idx)) = def_site.get(&i) {
349            cur_def.insert(*var, *idx);
350        }
351        if let Stmt::ExprStmt(Expr::Assign { target, value, .. }) = st {
352            if let Expr::Field {
353                cls,
354                name: fname,
355                is_static: true,
356                ..
357            } = &**target
358            {
359                if cls.as_ref() != class.name {
360                    continue;
361                }
362                if !const_fields.iter().any(|f| f.name.as_str() == &**fname) {
363                    continue;
364                }
365                if const_field.iter().any(|f| !f.is_empty() && f == &**fname) { return None; // duplicate assignment
366                }
367                let idx = match &**value {
368                    Expr::Local { var, .. } => cur_def.get(var).copied()?,
369                    Expr::New { cls: ncls, args, .. }
370                        if ncls.as_ref() == class.name && args.len() >= 2 =>
371                    {
372                        if let (Expr::Const(ConstVal::Str(n)), Expr::Const(ConstVal::Int(ord0))) =
373                            (&args[0], &args[1])
374                        {
375                            if java_ident(n).as_ref() != &**n || n.is_empty() { return None; }
376                            let idx = const_name.len();
377                            const_ord.push(*ord0 as i64);
378                            const_name.push(n.to_string());
379                            const_field.push(String::new());
380                            const_extra.push(resolve_enum_extras(
381                                &args[2..],
382                                &defs2,
383                                tainted2,
384                                &var_of,
385                                &const_name,
386                                &self_name,
387                                &self_ty,
388                            )?);
389                            idx
390                        } else {
391                            return None;
392                        }
393                    }
394                    _ => return None,
395                };
396                const_field[idx] = fname.to_string();
397                drop_stmts.push(i);
398            }
399        }
400        track_def(st, &mut defs2, &mut tainted2);
401    }
402    // Own the reaching-def values: defs2 borrows body.body, and the
403    // synthetic-constant scan below mutates the $VALUES array in place.
404    let defs2_owned: std::collections::HashMap<u32, Expr> = defs2
405        .iter()
406        .map(|(k, v)| (*k, (*v).clone()))
407        .collect();
408    drop(defs2);
409    let defs2: std::collections::HashMap<u32, &Expr> =
410        defs2_owned.iter().map(|(k, v)| (*k, v)).collect();
411
412    // Every ACC_ENUM field bound, every intermediate matched.
413    // Every ACC_ENUM FIELD must be bound to a constant. The converse
414    // need not hold: R8 drops the FIELD of a constant nothing reads
415    // externally while the $VALUES array keeps it (weixin lite/api/n's
416    // ON_DESTROY — a pass-1 local with no pass-2 sput). Field-less
417    // constants keep their ctor-string source name.
418    if const_field.iter().filter(|f| !f.is_empty()).count() != const_fields.len() {
419        return None;
420    }
421    // Obfuscated enums rename the ACC_ENUM FIELD (d/e/f) while the ctor's
422    // name STRING keeps the source identifier — the promoted constant
423    // must be declared under the FIELD name: every reference in the
424    // pool resolves through it. Using the name string broke all
425    // cross-file references (weixin +2.9k when newly-promoted enums
426    // swapped d/e/f for TEXT_ENTER_EDITING/…).
427    for i in 0..const_name.len() {
428        let field = &const_field[i];
429        if !field.is_empty() && field != &const_name[i] {
430            let id = java_ident(field);
431            if id.is_empty() || id.as_ref() != field.as_str() {
432                return None;
433            }
434            const_name[i] = field.clone();
435        }
436    }
437    {
438        let mut seen: std::collections::HashSet<&str> = std::collections::HashSet::default();
439        if !const_name.iter().all(|c| seen.insert(c.as_str())) { return None; }
440    }
441
442    // Synthetic constants: entries built INLINE inside the $VALUES
443    // array (`new Self("NAME", ord, ..)` with no ACC_ENUM field — R8
444    // drops the field when nothing outside reads it; weixin u2/i's
445    // "CONSTANT"). Promote them so the header declares the constant:
446    // the raw `new` in the array is "无法实例化枚举类", and every
447    // constant after the gap carries a SHIFTED ordinal (e was 2 in the
448    // dex, renders as position 1).
449    let mut synth: Vec<(i64, String, Vec<Expr>)> = Vec::new();
450    {
451        let self_cls: &str = class.name.as_str();
452        if let Stmt::Block(vs) = &mut body.body {
453            for st in vs.iter_mut() {
454                crate::passes::walk_stmt_exprs(st, &mut |e| {
455                    crate::passes::deep_rewrite(e, &mut |x| {
456                        let Expr::NewArray { elem, init: Some(list), .. } = x else {
457                            return;
458                        };
459                        if elem.erased() != JavaType::Object(self_cls.into()) {
460                            return;
461                        }
462                        for slot in list.iter_mut() {
463                            let (n, ord, rest) = match slot {
464                                Expr::New { cls, args, raw: false, .. }
465                                    if cls.as_ref() == self_cls && args.len() >= 2 =>
466                                {
467                                    match (&args[0], &args[1]) {
468                                        (
469                                            Expr::Const(ConstVal::Str(sv)),
470                                            Expr::Const(ConstVal::Int(o)),
471                                        ) => (sv.to_string(), *o as i64, args[2..].to_vec()),
472                                        _ => continue,
473                                    }
474                                }
475                                _ => continue,
476                            };
477                            if java_ident(&n).as_ref() != n.as_str() || n.is_empty() {
478                                continue;
479                            }
480                            if const_name.contains(&n)
481                                || synth.iter().any(|(_, s2, _)| *s2 == n)
482                            {
483                                continue;
484                            }
485                            let Some(extras) = resolve_enum_extras(
486                                &rest,
487                                &defs2,
488                                tainted2,
489                                &var_of,
490                                &const_name,
491                                &self_name,
492                                &self_ty,
493                            ) else {
494                                continue;
495                            };
496                            *slot = Expr::Field {
497                                owner: None,
498                                cls: self_name.clone(),
499                                name: std::sync::Arc::from(n.as_str()),
500                                ty: self_ty.clone(),
501                                is_static: true,
502                            };
503                            synth.push((ord, n, extras));
504                        }
505                    });
506                });
507            }
508        }
509    }
510
511    // Pass 3: rewrite references to the intermediate locals into the
512    // constant identifiers (static-field reads on Self). POSITION-AWARE:
513    // a REUSED local must read as the constant defined most recently at
514    // that statement — a var-level map made every `arr[k] = v` in the
515    // $VALUES build reference the LAST constant (weixin +2.9k when this
516    // regressed values() contents).
517    {
518        let mut cur: HashMap<u32, usize> = HashMap::default();
519        if let Stmt::Block(vs) = &mut body.body {
520            for (i, st) in vs.iter_mut().enumerate() {
521                if let Some((var, idx)) = def_site.get(&i) {
522                    cur.insert(*var, *idx);
523                }
524                crate::passes::walk_stmt_exprs(st, &mut |e| {
525                    crate::passes::deep_rewrite(e, &mut |x| {
526                        if let Expr::Local { var, .. } = x {
527                            if let Some(&idx) = cur.get(var) {
528                                let name: std::sync::Arc<str> =
529                                    std::sync::Arc::from(const_name[idx].as_str());
530                                *x = Expr::Field {
531                                    owner: None,
532                                    cls: self_name.clone(),
533                                    name,
534                                    ty: self_ty.clone(),
535                                    is_static: true,
536                                };
537                            }
538                        }
539                    });
540                });
541            }
542        }
543    }
544
545    // Pass 4: drop the definitions and their sputs.
546    if let Stmt::Block(vs) = &mut body.body {
547        let drop_set: std::collections::HashSet<usize> =
548            drop_stmts.iter().copied().collect();
549        vs.retain(|_| true);
550        let mut idx = 0usize;
551        vs.retain(|_| {
552            let keep = !drop_set.contains(&idx);
553            idx += 1;
554            keep
555        });
556    }
557    // The resolved args consumed the clinit locals' readers; prune the
558    // now-dead defs from the remnant (impure inits survive as bare
559    // expression statements — drop_dead_locals' standard contract).
560    crate::passes::drop_dead_locals(&mut body.body);
561
562    let mut merged: Vec<(i64, EnumConst)> = const_ord
563        .into_iter()
564        .zip(
565            const_field
566                .into_iter()
567                .zip(const_name)
568                .zip(const_extra)
569                .map(|((field, name), extra_args)| EnumConst {
570                    field,
571                    name,
572                    extra_args,
573                }),
574        )
575        .collect();
576    for (ord, name, extra_args) in synth {
577        merged.push((
578            ord,
579            EnumConst {
580                field: name.clone(),
581                name,
582                extra_args,
583            },
584        ));
585    }
586    if merged.is_empty() {
587        return None; // nothing extracted — keep the desugared form
588    }
589    merged.sort_by_key(|(o, _)| *o);
590    // Ordinals must be UNIQUE and ASCENDING; gaps are legal — R8 drops
591    // unused constants wholesale (weixin lite/api/n starts at ordinal
592    // 1). Java derives ordinal() from declaration position, so a gap
593    // shifts every later constant. Pad each gap with a synthetic
594    // constant (`_r<k>`) to keep positions faithful — only when no
595    // constant carries extra ctor args (a pad cannot invent them).
596    // The synthetic values()/valueOf() the true-enum render suppresses
597    // then see the pad (values() drift vs the dex $VALUES array — the
598    // compilable-and-ordinal-faithful side of the trade).
599    let has_extras = merged.iter().any(|(_, c)| !c.extra_args.is_empty());
600    let mut member_taken: jdc_core::FxHashSet<String> = merged
601        .iter()
602        .map(|(_, c)| c.name.clone())
603        .chain(
604            class
605                .static_fields
606                .iter()
607                .chain(class.instance_fields.iter())
608                .map(|f| f.name.to_string()),
609        )
610        .chain(class.all_methods().map(|m| m.name.to_string()))
611        .collect();
612    let mut padded: Vec<EnumConst> = Vec::with_capacity(merged.len());
613    let mut expect: i64 = 0;
614    let mut pad_k = 0u32;
615    for (ord, c) in merged {
616        if ord < expect {
617            return None; // duplicate/colliding ordinals — unfaithful
618        }
619        if ord > expect {
620            // Extras-bearing ctors: a pad may borrow an adjacent
621            // constant's literal extras ONLY when every ctor of the
622            // class is benign (no branch, no throw, no non-<init>
623            // invoke — pure param stores), so the invented value
624            // cannot change runtime behavior beyond a dead constant's
625            // stored field (jd1.u: gap at ordinal 7 + int extra arg
626            // aborted the whole promotion — 92 files of broken
627            // Enum.valueOf). Non-benign ctors keep the abort.
628            let tmpl: Vec<Expr> = if has_extras {
629                if !enum_ctors_benign(pool, class) {
630                    return None; // cannot synthesize the missing ctor args
631                }
632                match padded.last() {
633                    Some(prev) => prev.extra_args.clone(),
634                    None => c.extra_args.clone(),
635                }
636            } else {
637                Vec::new()
638            };
639            while expect < ord {
640                let pname = loop {
641                    let cand = format!("_r{pad_k}");
642                    pad_k += 1;
643                    if !member_taken.contains(&cand) {
644                        member_taken.insert(cand.clone());
645                        break cand;
646                    }
647                };
648                padded.push(EnumConst {
649                    field: pname.clone(),
650                    name: pname,
651                    extra_args: tmpl.clone(),
652                });
653                expect += 1;
654            }
655        }
656        expect = ord + 1;
657        padded.push(c);
658    }
659    let out = padded;
660    Some((out, body))
661}
662
663/// True when every `<init>` of the class is a pure param-store: no
664/// branch, no throw, no switch, no goto, and no invoke other than a
665/// constructor call (the super delegation). A benign ctor cannot
666/// validate or leak its arguments, so gap-pad constants may borrow a
667/// neighbor's literal extras without observable effect.
668fn enum_ctors_benign(pool: &DexPool, class: &PoolClass) -> bool {
669    let mut checked = 0usize;
670    for m in class.all_methods() {
671        if &*m.name != "<init>" {
672            continue;
673        }
674        checked += 1;
675        let Some(dex) = pool.dex(m.dex_idx) else {
676            return false;
677        };
678        let Some(ci) = dex.code_at(m.code_off) else {
679            return false;
680        };
681        for ins in &ci.insns {
682            use ddc_dex::insn::InsnKind;
683            match ins.kind {
684                InsnKind::If { .. }
685                | InsnKind::Throw { .. }
686                | InsnKind::PackedSwitch { .. }
687                | InsnKind::SparseSwitch { .. }
688                | InsnKind::Goto { .. } => return false,
689                InsnKind::Invoke { method_idx, .. } => {
690                    let mr = dex.method(method_idx);
691                    if dex.string(mr.name_idx) != "<init>" {
692                        return false;
693                    }
694                }
695                _ => {}
696            }
697        }
698    }
699    checked > 0
700}
701
702/// Update the rolling reaching-def map for enum-arg resolution. Only
703/// flat single-target definitions keep the tracking sound; any control
704/// flow taints it (a linear scan can no longer prove WHICH definition
705/// reaches later capture sites).
706fn track_def<'e>(
707    st: &'e Stmt,
708    defs: &mut std::collections::HashMap<u32, &'e Expr>,
709    tainted: &mut bool,
710) {
711    match st {
712        Stmt::LocalDef { var, init, .. } => match init {
713            Some(e) => {
714                defs.insert(*var, e);
715            }
716            None => {
717                defs.remove(var);
718            }
719        },
720        Stmt::ExprStmt(Expr::Assign { target, value, op, .. }) => {
721            if let Expr::Local { var, .. } = &**target {
722                if matches!(op, jdc_core::ir::expr::AssignOp::Plain) {
723                    defs.insert(*var, value);
724                } else {
725                    defs.remove(var);
726                }
727            }
728        }
729        // Plain expression statements (calls) don't define locals.
730        Stmt::ExprStmt(_) => {}
731        _ => *tainted = true,
732    }
733}
734
735/// Resolve every enum-ctor extra arg to a self-contained expression, or
736/// reject the enum mode (None). R8 reuses ONE register across all
737/// constant constructions — revenuecat's LogIntent builds 11 of 12
738/// emoji lists through the same `list` local, reassigned between the
739/// `new Self(.., list)` sites — and enum constants render OUTSIDE the
740/// clinit where no local is in scope: an unresolved `Local` used to
741/// print as the vt-dummy name (`DEBUG(var0)` — per-constant
742/// cannot-find). Each Local is replaced by its reaching pure definition
743/// (recursively) or, when it names an earlier constant's intermediate,
744/// by a static-field reference to that constant. Any expression shape
745/// WITHOUT locals passes through untouched (enum args may be arbitrary
746/// expressions — BinOp/Cast/Method/New — each renders exactly once per
747/// constant, so no duplication concern applies). Only an unresolvable
748/// Local (missing/tainted def, depth > 4) rejects the whole enum
749/// detection; the class then falls back to plain-field rendering, which
750/// always compiles.
751fn resolve_enum_extras(
752    extras: &[Expr],
753    defs: &std::collections::HashMap<u32, &Expr>,
754    tainted: bool,
755    var_of: &std::collections::HashMap<u32, usize>,
756    const_name: &[String],
757    self_name: &std::sync::Arc<str>,
758    self_ty: &jdc_core::ir::expr::TypeRef,
759) -> Option<Vec<Expr>> {
760    extras
761        .iter()
762        .map(|e| {
763            let mut out = e.clone();
764            let mut fail = false;
765            resolve_locals_in(
766                &mut out, defs, tainted, var_of, const_name, self_name, self_ty, 0,
767                &mut fail,
768            );
769            if fail {
770                None
771            } else {
772                Some(out)
773            }
774        })
775        .collect()
776}
777
778#[allow(clippy::too_many_arguments)]
779fn resolve_locals_in(
780    e: &mut Expr,
781    defs: &std::collections::HashMap<u32, &Expr>,
782    tainted: bool,
783    var_of: &std::collections::HashMap<u32, usize>,
784    const_name: &[String],
785    self_name: &std::sync::Arc<str>,
786    self_ty: &jdc_core::ir::expr::TypeRef,
787    depth: u32,
788    fail: &mut bool,
789) {
790    crate::passes::deep_rewrite(e, &mut |x| {
791        if let Expr::Local { var, .. } = x {
792            // An earlier constant's intermediate: a static-field read of
793            // that constant (declared above — backward reference, legal
794            // in enum ctor args).
795            if let Some(&ci) = var_of.get(var) {
796                if let Some(nm) = const_name.get(ci) {
797                    *x = Expr::Field {
798                        owner: None,
799                        cls: self_name.clone(),
800                        name: std::sync::Arc::from(nm.as_str()),
801                        ty: self_ty.clone(),
802                        is_static: true,
803                    };
804                    return;
805                }
806            }
807            if tainted || depth > 4 {
808                *fail = true;
809                return;
810            }
811            let Some(d) = defs.get(var).copied() else {
812                *fail = true;
813                return;
814            };
815            let mut sub = d.clone();
816            resolve_locals_in(
817                &mut sub, defs, tainted, var_of, const_name, self_name, self_ty,
818                depth + 1, fail,
819            );
820            if *fail {
821                return;
822            }
823            *x = sub;
824        }
825    });
826}
827
828/// Render enum-constant constructor arguments via the shared expression
829/// emitter (the VarTable is irrelevant for argument printing — no local
830/// names appear — but the API requires one).
831fn render_enum_args(ctx: &DexCtx<'_>, pool: &DexPool, args: &[Expr], out: &mut String) {
832    let dummy_vt = jdc_core::var::VarTable::default();
833    let mut p = Printer::new(ctx, &dummy_vt);
834    for (i, a) in args.iter().enumerate() {
835        if i > 0 {
836            out.push_str(", ");
837        }
838        p.expr(a, 0, out);
839    }
840    let _ = pool;
841}
842
843/// Synthesize `throw new AbstractMethodError()` stubs for abstract
844/// interface methods a CONCRETE class fails to implement. R8 tree-shakes
845/// an interface method whose call sites it proved unreachable, leaving a
846/// concrete class ART accepts but javac rejects ("X不是抽象的, 并且未覆盖
847/// 抽象方法…"). jadx reproduces the same incomplete class; the stub
848/// restores compilability AND matches ART semantics exactly (invoking a
849/// stripped abstract method throws AbstractMethodError). `provided` is
850/// computed from the DEX method table (not the render), so a method ddc
851/// merely failed to render is never double-declared, and a same-erasure
852/// return-type-specialized sibling (Family B: void `invoke` beside the
853/// Object bridge) counts as provided → no colliding stub.
854fn synth_missing_interface_stubs(
855    pool: &DexPool,
856    class: &PoolClass,
857    out: &mut String,
858    depth: usize,
859    emitted_any: &mut bool,
860) {
861    use crate::access::*;
862    // Concrete classes only: interfaces / abstract classes / annotations
863    // legitimately leave methods unimplemented.
864    if class.access & (ACC_INTERFACE | ACC_ABSTRACT | ACC_ANNOTATION) != 0 {
865        return;
866    }
867    if class.interfaces.is_empty() {
868        return;
869    }
870    fn argsig(d: &str) -> &str {
871        let lo = d.find('(').map(|i| i + 1).unwrap_or(0);
872        let hi = d.find(')').unwrap_or(d.len());
873        &d[lo..hi]
874    }
875    // Transitive interface closure (pool-resolvable only; framework
876    // interfaces live in android.jar and javac already knows them).
877    let mut required: jdc_core::FxHashMap<(String, String), &PoolMethod> =
878        jdc_core::FxHashMap::default();
879    let mut defaults: jdc_core::FxHashSet<(String, String)> = jdc_core::FxHashSet::default();
880    let mut stack: Vec<&String> = class.interfaces.iter().collect();
881    let mut seen: jdc_core::FxHashSet<&str> = jdc_core::FxHashSet::default();
882    while let Some(iname) = stack.pop() {
883        if !seen.insert(iname.as_str()) {
884            continue;
885        }
886        let Some(ic) = pool.get(iname) else { continue };
887        for m in ic.all_methods() {
888            if m.is_static() || &*m.name == "<clinit>" || &*m.name == "<init>" {
889                continue;
890            }
891            let key = (m.name.to_string(), argsig(&m.desc).to_string());
892            if m.code_off == 0 {
893                required.entry(key).or_insert(m);
894            } else {
895                defaults.insert(key);
896            }
897        }
898        stack.extend(ic.interfaces.iter());
899    }
900    if required.is_empty() {
901        return;
902    }
903    // Methods provided by the class + its concrete superclass chain, plus
904    // the java/lang/Object publics that satisfy common requirements.
905    let mut provided: jdc_core::FxHashSet<(String, String)> = jdc_core::FxHashSet::default();
906    provided.insert(("equals".into(), "Ljava/lang/Object;".into()));
907    provided.insert(("hashCode".into(), String::new()));
908    provided.insert(("toString".into(), String::new()));
909    let mut cur: Option<&PoolClass> = Some(class);
910    while let Some(c) = cur {
911        for m in c.all_methods() {
912            // A method PROVIDES the implementation if it is not abstract:
913            // bytecode (code_off != 0) OR native (JNI body, code_off == 0
914            // but ACC_NATIVE — `…ToNative` methods satisfy their interface
915            // and must not be stubbed twice).
916            if m.access & ACC_ABSTRACT == 0 && !m.is_static() {
917                provided.insert((m.name.to_string(), argsig(&m.desc).to_string()));
918            }
919        }
920        cur = c.super_name.as_ref().and_then(|s| {
921            if s == "java/lang/Object" {
922                None
923            } else {
924                pool.get(s)
925            }
926        });
927    }
928    let mut missing: Vec<&PoolMethod> = Vec::new();
929    for (key, m) in &required {
930        if defaults.contains(key) || provided.contains(key) {
931            continue;
932        }
933        missing.push(*m);
934    }
935    missing.sort_by(|a, b| (&*a.name, &*a.desc).cmp(&(&*b.name, &*b.desc)));
936    let ind = "    ".repeat(depth + 1);
937    for m in missing {
938        let Some(d) = m.parsed_desc() else { continue };
939        if *emitted_any {
940            out.push('\n');
941        }
942        out.push_str(&ind);
943        out.push_str("public ");
944        out.push_str(&type_name(pool, &d.ret));
945        out.push(' ');
946        out.push_str(&java_ident(&m.name));
947        out.push('(');
948        for (i, a) in d.args.iter().enumerate() {
949            if i > 0 {
950                out.push_str(", ");
951            }
952            out.push_str(&type_name(pool, a));
953            out.push_str(&format!(" p{}", i + 1));
954        }
955        out.push_str(") {\n");
956        out.push_str(&ind);
957        out.push_str("    throw new AbstractMethodError();\n");
958        out.push_str(&ind);
959        out.push_str("}\n");
960        *emitted_any = true;
961    }
962}
963
964/// Synthesize `ordinal()` (and `values()` when the dex copy was stripped)
965/// for a FALLBACK enum base (`/* enum */ class`, restoration failed). The
966/// fallback renders as a plain class that cannot extend java.lang.Enum, so
967/// the inherited `ordinal()` is absent and every `switch (x.ordinal())`
968/// fails ("找不到符号 方法 ordinal()", weibo protobuf ub ×206, weixin ×63).
969/// The ordinal IS the constant's position in the $VALUES array (which the
970/// dex `values()` already clones), so recover it by linear scan. Purely
971/// additive: the dex never declares ordinal() (inherited from Enum), so
972/// there is no collision. Only the BASE (super Enum/Object) — constant
973/// subclasses inherit it.
974fn synth_enum_ordinal(
975    pool: &DexPool,
976    class: &PoolClass,
977    out: &mut String,
978    depth: usize,
979    emitted_any: &mut bool,
980) {
981    use crate::access::*;
982    let is_base = class
983        .super_name
984        .as_ref()
985        .map(|s| s == "java/lang/Enum" || s == "java/lang/Object")
986        .unwrap_or(true);
987    if !class.is_enum() || !is_base {
988        return;
989    }
990    // Never declared on the dex enum (inherited from Enum), but guard.
991    if class
992        .all_methods()
993        .any(|m| &*m.name == "ordinal" && &*m.desc == "()I")
994    {
995        return;
996    }
997    let self_arr = JavaType::Array(Box::new(JavaType::Object(class.name.as_str().into())));
998    let arr_name = type_name(pool, &self_arr);
999    let has_values = class.all_methods().any(|m| {
1000        &*m.name == "values"
1001            && m.access & ACC_STATIC != 0
1002            && m.parsed_desc()
1003                .map(|d| d.ret == self_arr && d.args.is_empty())
1004                .unwrap_or(false)
1005    });
1006    let ind = "    ".repeat(depth + 1);
1007    if !has_values {
1008        // $VALUES array field: the synthetic `static final Self[]`.
1009        let want = format!("[L{};", class.name);
1010        let Some(vf) = class.static_fields.iter().find(|f| f.desc == want) else {
1011            return; // no array to recover ordinals from
1012        };
1013        if *emitted_any {
1014            out.push('\n');
1015        }
1016        out.push_str(&ind);
1017        out.push_str(&format!("public static {} values() {{\n", arr_name));
1018        out.push_str(&ind);
1019        out.push_str(&format!(
1020            "    return ({}) {}.clone();\n",
1021            arr_name,
1022            java_ident(&vf.name)
1023        ));
1024        out.push_str(&ind);
1025        out.push_str("}\n");
1026        *emitted_any = true;
1027    }
1028    if *emitted_any {
1029        out.push('\n');
1030    }
1031    out.push_str(&ind);
1032    out.push_str("public int ordinal() {\n");
1033    out.push_str(&ind);
1034    out.push_str(&format!("    {} vs = values();\n", arr_name));
1035    out.push_str(&ind);
1036    out.push_str("    for (int i = 0; i < vs.length; i++) {\n");
1037    out.push_str(&ind);
1038    out.push_str("        if (vs[i] == this) {\n");
1039    out.push_str(&ind);
1040    out.push_str("            return i;\n");
1041    out.push_str(&ind);
1042    out.push_str("        }\n");
1043    out.push_str(&ind);
1044    out.push_str("    }\n");
1045    out.push_str(&ind);
1046    out.push_str("    return 0;\n");
1047    out.push_str(&ind);
1048    out.push_str("}\n");
1049    *emitted_any = true;
1050}
1051
1052#[allow(clippy::only_used_in_recursion)]
1053fn emit_class_body(
1054    pool: &DexPool,
1055    class: &PoolClass,
1056    ctx: &DexCtx<'_>,
1057    opts: &ClassOptions,
1058    out: &mut String,
1059    depth: usize,
1060) -> anyhow::Result<()> {
1061    let ind = indent(depth);
1062    // The class's own header must use the RENAMED display name — the
1063    // ctor path (is_init) already renames; a header declaring `class a`
1064    // while the file/ctor say `a_2` leaves the ctor looking like a
1065    // method with no return type (MinisApp's Y2.a vs y2.a package
1066    // case-collision: 2,518 javac parse errors).
1067    let cname = crate::apply_class_rename(&class.name);
1068    let (_, simple) = split_name(&cname);
1069    // A class emitted as its OWN top-level file (depth 0) must declare a
1070    // flat `$` name — `class Outer.Inner` is not declarable at file
1071    // scope. An INLINE nested member (depth > 0) declares its own
1072    // segment inside the parent's body.
1073    let simple = if depth == 0 {
1074        simple.to_string()
1075    } else {
1076        // R8 names can END in `$`: an empty last `$`-segment would
1077        // render `class  {` — keep the whole simple name.
1078        let seg = simple.rsplit('$').next().unwrap_or(&simple);
1079        if seg.is_empty() {
1080            simple.to_string()
1081        } else {
1082            seg.to_string()
1083        }
1084    };
1085    // A class NAMED `var`-style (taobao ships `tb.var`) cannot be
1086    // declared — restricted contextual type names escape here and in
1087    // the ctor, file name, and every type reference.
1088    let simple = if is_restricted_type_name(&simple) {
1089        format!("_{simple}")
1090    } else {
1091        simple
1092    };
1093    let is_iface = class.is_interface();
1094    let is_enum = class.is_enum();
1095
1096    let mut head = String::new();
1097    let a = class.access;
1098    if a & ACC_PUBLIC != 0 || widen_class(&class.name) {
1099        head.push_str("public ");
1100    }
1101    // Inline nested members need their `static` (interfaces/annotations
1102    // are implicitly static; a missing `static` on a member class makes
1103    // every `new Report(...)` site an "outer instance required" error).
1104    if depth > 0 && !is_iface && ctx.nested_is_static(&class.name) {
1105        head.push_str("static ");
1106    }
1107    if a & ACC_FINAL != 0 && !is_enum {
1108        head.push_str("final ");
1109    }
1110    if a & ACC_ABSTRACT != 0 && !is_iface {
1111        head.push_str("abstract ");
1112    }
1113    if a & ACC_ANNOTATION != 0 {
1114        head.push('@');
1115    }
1116    let mut enum_consts: Option<(Vec<EnumConst>, crate::method::MethodBody)> = if is_enum {
1117        collect_enum_constants(pool, class, ctx)
1118    } else {
1119        None
1120    };
1121    if let Some(ecs) = &enum_consts {
1122        // True `enum` declaration: constants render in the header, the
1123        // desugared boilerplate (const fields, their <clinit> inits —
1124        // stripped by strip_enum_const_inits — and the ACC_ENUM flags)
1125        // disappears. R8-renamed values()/valueOf() stay (they do not
1126        // collide with the compiler-generated ones); javac-named ones
1127        // are skipped at the method loop below.
1128        head.push_str("enum ");
1129        head.push_str(&sanitize_ref(&simple));
1130        let _ = ecs;
1131    } else if is_enum {
1132        // An enum with constant-specific bodies carries ACC_ABSTRACT —
1133        // `abstract final` is an illegal modifier combination; abstract
1134        // (already pushed above) suppresses the hardcoded final.
1135        let abstract_ = a & ACC_ABSTRACT != 0;
1136        head.push_str(if abstract_ {
1137            "/* enum */ class "
1138        } else {
1139            "/* enum */ final class "
1140        });
1141        head.push_str(&sanitize_ref(&simple));
1142    } else if is_iface {
1143        head.push_str("interface ");
1144        head.push_str(&sanitize_ref(&simple));
1145    } else {
1146        head.push_str("class ");
1147        head.push_str(&sanitize_ref(&simple));
1148    }
1149    // The obscured-super import was emitted at the package line; the
1150    // clause must render the SIMPLE name (the qualified form binds to
1151    // the class itself even with the import present).
1152    let render_super = |sup: &String| -> String { print_class_name(pool, sup) };
1153    if is_iface {
1154        // A @interface cannot declare extends at all (JLS 9.6): the dex
1155        // interface table lists java/lang/annotation/Annotation for
1156        // every annotation type — rendering it produced "对于
1157        // @interfaces, 不允许 'extends'" (weibo 1100).
1158        let is_annot = a & ACC_ANNOTATION != 0;
1159        if !class.interfaces.is_empty() && !is_annot {
1160            head.push_str(" extends ");
1161            head.push_str(&join_dotted(pool, &class.interfaces));
1162        }
1163    } else {
1164        if let Some(sup) = &class.super_name {
1165            // Suppress `extends` for TRUE `enum` declarations (they
1166            // implicitly extend java.lang.Enum) and for the enum BASE in
1167            // fallback form (super IS java/lang/Enum — `class X extends
1168            // Enum` is illegal). But an ENUM CONSTANT SUBCLASS rendered
1169            // in fallback (`/* enum */ class j$1`, ACC_ENUM, super = the
1170            // enum base) MUST keep `extends j`: without it the constant
1171            // loses its subtype relation and every `field = new j$1(..)`
1172            // fails ("j$1无法转换为j", weibo jsoup TokeniserState ×172).
1173            let suppress_extends =
1174                is_enum && (enum_consts.is_some() || sup == "java/lang/Enum");
1175            if sup != "java/lang/Object" && !suppress_extends {
1176                head.push_str(" extends ");
1177                head.push_str(&render_super(sup));
1178            }
1179        }
1180        if !class.interfaces.is_empty() {
1181            head.push_str(if is_iface {
1182                " extends "
1183            } else {
1184                " implements "
1185            });
1186            head.push_str(&join_dotted(pool, &class.interfaces));
1187        }
1188    }
1189
1190    out.push_str(&ind);
1191    out.push_str(&head);
1192    out.push_str(" {\n");
1193
1194    // True-enum constant list: the constants lead the body, ahead of
1195    // any remaining fields.
1196    if let Some((ecs, _)) = &enum_consts {
1197        for (i, ec) in ecs.iter().enumerate() {
1198            out.push_str(&indent(depth + 1));
1199            out.push_str(&java_ident(&ec.name));
1200            if !ec.extra_args.is_empty() {
1201                let mut line = String::from("(");
1202                render_enum_args(ctx, pool, &ec.extra_args, &mut line);
1203                line.push(')');
1204                out.push_str(&line);
1205            }
1206            if i + 1 < ecs.len() {
1207                out.push_str(",\n");
1208            } else {
1209                out.push_str(";\n");
1210            }
1211        }
1212        out.push('\n');
1213    }
1214
1215    // Fields.
1216    // A static final whose dex-encoded static value is OVERWRITTEN by
1217    // the clinit must render as a blank final: the dex encoded array
1218    // value is just the class-load default (ART lets <clinit> reassign
1219    // static finals), but Java forbids touching an initialized final
1220    // ("无法为 static final 变量 $stable 分配值" — Compose `$stable = 0`
1221    // + clinit `= 8`; WalletBaseUI HARDCODE_* = 0 + clinit computed
1222    // value; the original null-valued shape: Kotlin `object` INSTANCE,
1223    // okio SegmentPool). Scan the clinit's raw SPuts once.
1224    let clinit_sputs: Option<jdc_core::FxHashSet<(std::sync::Arc<str>, std::sync::Arc<str>)>> =
1225        // Gate: only classes with a VALUED final static can hit the
1226        // conflict — skip the clinit decode otherwise (decoding it for
1227        // every class cost seconds on 98k-class corpora).
1228        (|| {
1229            let has_valued_final = class.static_fields.iter().enumerate().any(|(i, f)| {
1230                f.access & crate::access::ACC_FINAL != 0
1231                    && class.static_values.get(i).is_some()
1232            });
1233            if !has_valued_final {
1234                return None;
1235            }
1236            let m = class.all_methods().find(|m| &*m.name == "<clinit>")?;
1237            let dex = pool.dex(m.dex_idx)?;
1238            let ci = dex.code_at(m.code_off)?;
1239            let mut set: jdc_core::FxHashSet<(
1240                std::sync::Arc<str>,
1241                std::sync::Arc<str>,
1242            )> = jdc_core::FxHashSet::default();
1243            for ins in &ci.insns {
1244                if let InsnKind::SPut { field_idx, .. } = ins.kind {
1245                    let fid = dex.field(field_idx);
1246                    // clinit only writes own-class fields here; match
1247                    // (name, type) against the field list below.
1248                    set.insert((
1249                        std::sync::Arc::from(dex.string(fid.name_idx)),
1250                        std::sync::Arc::from(dex.type_name(fid.type_idx)),
1251                    ));
1252                }
1253            }
1254            Some(set)
1255        })();
1256    let mut field_emitted = false;
1257    for (i, f) in class.static_fields.iter().enumerate() {
1258        // Enum constant fields became the header list above.
1259        if enum_consts.is_some() && f.access & crate::access::ACC_ENUM != 0 {
1260            continue;
1261        }
1262        if field_emitted || !class.instance_fields.is_empty() {
1263            out.push('\n');
1264        }
1265        field_emitted = true;
1266        // A blank-final: static final, clinit-assigned. Interfaces are
1267        // excluded — their fields are implicitly `public static final`
1268        // and MUST carry an initializer.
1269        let blank_final = f.access & crate::access::ACC_FINAL != 0
1270            && !class.is_interface()
1271            && clinit_sputs
1272                .as_ref()
1273                .is_some_and(|s| {
1274                    s.contains(&(
1275                        std::sync::Arc::from(f.name.as_str()),
1276                        std::sync::Arc::from(f.desc.as_str()),
1277                    ))
1278                });
1279        emit_field(
1280            pool,
1281            f,
1282            if blank_final {
1283                None
1284            } else {
1285                class.static_values.get(i)
1286            },
1287            &class.name,
1288            out,
1289            depth + 1,
1290            true,
1291            class.is_interface(),
1292        );
1293    }
1294    if !class.instance_fields.is_empty() && !class.static_fields.is_empty() {
1295        out.push('\n');
1296    }
1297    for (i, f) in class.instance_fields.iter().enumerate() {
1298        if i > 0 {
1299            out.push('\n');
1300        }
1301        emit_field(pool, f, None, &class.name, out, depth + 1, false, false);
1302    }
1303
1304    // Methods.
1305    // Duplicated (name, parameter-types) pairs in one class are javac
1306    // "method is already defined" errors (weibo: 11.5k hits). Source
1307    // cannot declare them, so exactly one survives: covariant bridges
1308    // (`Object get(int)` beside `ByteString get(int)` — the compiler
1309    // GENERATES bridges), plus R8 output that lost the bridge flag
1310    // (okio Buffer.clone, interface getView re-declarations). A
1311    // non-bridge method outranks a bridge for the same signature
1312    // (bridge bodies are delegation stubs); first occurrence otherwise.
1313    // Never drop a signature outright — the last method standing for a
1314    // key is always rendered.
1315    // Keyed on the DISPLAY name: a member-renamed method (return-type-
1316    // only collisions on isolated classes) no longer collides with its
1317    // sibling — both render, each caller resolving through the registry.
1318    fn sig_key<'m>(cls: &str, renames_on: bool, m: &'m PoolMethod) -> (&'m str, &'m str) {
1319        let d: &'m str = &m.desc;
1320        let lo = d.find('(').map(|i| i + 1).unwrap_or(0);
1321        let hi = d.find(')').unwrap_or(d.len());
1322        let n = if renames_on {
1323            jdc_core::rename::field_display(cls, &m.name, d).unwrap_or(&*m.name)
1324        } else {
1325            &*m.name
1326        };
1327        (n, &d[lo..hi])
1328    }
1329    let renames_on = jdc_core::rename::member_rename_active();
1330    let methods: Vec<&PoolMethod> = class.all_methods().collect();
1331    let mut claim: jdc_core::FxHashMap<(&str, &str), usize> = jdc_core::FxHashMap::default();
1332    for (i, m) in methods.iter().enumerate() {
1333        let key = sig_key(&class.name, renames_on, m);
1334        match claim.get(&key) {
1335            Some(&j)
1336                if m.access & crate::access::ACC_BRIDGE == 0
1337                    && methods[j].access & crate::access::ACC_BRIDGE != 0 =>
1338            {
1339                claim.insert(key, i);
1340            }
1341            None => {
1342                claim.insert(key, i);
1343            }
1344            _ => {}
1345        }
1346    }
1347    // NOTE: a same-signature bridge is already retired by the claim map
1348    // above. An ERASURE-shaped bridge (SAM/variance: params are the
1349    // Object-erased types of a sibling's) must NOT be skipped: with raw
1350    // (non-generic) interface rendering the bridge is exactly what
1351    // satisfies the interface — skipping it turned every Kotlin lambda
1352    // class into "不是抽象的, 并且未覆盖…invoke(Object,Object)" (reqable
1353    // +17), while the "对invoke的引用不明确" it appeared to fix was a
1354    // missing-kotlin-classpath cascade, not a ddc bug.
1355    let mut emitted_any = !class.static_fields.is_empty() || !class.instance_fields.is_empty();
1356    for (i, m) in methods.iter().enumerate() {
1357        if &*m.name == "<clinit>" {
1358            continue; // rendered after the fields
1359        }
1360        if claim.get(&sig_key(&class.name, renames_on, m)) != Some(&i) {
1361            continue;
1362        }
1363        // True-enum rendering: javac auto-generates values()/valueOf()
1364        // — the original-named ones must not re-declare (R8-renamed
1365        // copies stay, they do not collide).
1366        if let Some((ecs, _)) = &enum_consts {
1367            let d = m.parsed_desc();
1368            let self_arr = d.as_ref().map(|d| d.ret == JavaType::Array(Box::new(JavaType::Object(class.name.as_str().into()))));
1369            let self_ret = d.as_ref().map(|d| d.ret == JavaType::Object(class.name.as_str().into()));
1370            let no_args = d.as_ref().map(|d| d.args.is_empty()).unwrap_or(false);
1371            let one_str = d
1372                .as_ref()
1373                .map(|d| d.args.len() == 1 && matches!(d.args[0], JavaType::Object(ref n) if n.as_ref() == "java/lang/String"))
1374                .unwrap_or(false);
1375            let static_ = m.is_static();
1376            if static_ && no_args && self_arr == Some(true) && &*m.name == "values" {
1377                continue;
1378            }
1379            if static_ && one_str && self_ret == Some(true) && &*m.name == "valueOf" {
1380                continue;
1381            }
1382            // Enum constructors must be private in source form.
1383            let m_owned: Option<PoolMethod> = if &*m.name == "<init>" {
1384                let mut c = (*m).clone();
1385                c.access = (c.access & !(crate::access::ACC_PUBLIC | crate::access::ACC_PROTECTED | crate::access::ACC_PRIVATE)) | crate::access::ACC_PRIVATE;
1386                Some(c)
1387            } else {
1388                None
1389            };
1390            let m_ref: &PoolMethod = m_owned.as_ref().unwrap_or(m);
1391            let mark = out.len();
1392            if emitted_any {
1393                out.push('\n');
1394            }
1395            if emit_method(pool, class, ctx, m_ref, depth + 1, true, out)? {
1396                emitted_any = true;
1397            } else {
1398                out.truncate(mark);
1399            }
1400            let _ = ecs;
1401            continue;
1402        }
1403        let mark = out.len();
1404        if emitted_any {
1405            out.push('\n');
1406        }
1407        if emit_method(pool, class, ctx, m, depth + 1, enum_consts.is_some(), out)? {
1408            emitted_any = true;
1409        } else {
1410            out.truncate(mark);
1411        }
1412    }
1413    // Inherited-ctor bridges: dex method refs resolve through the
1414    // hierarchy, so `new C(args)` against a class C with NO declared
1415    // <init> legally targets the SUPERCLASS ctor (weixin tenpay
1416    // `new m(map)` — m declares nothing, i.<init>(HashMap) does). Java
1417    // has no inherited constructors: without a bridge, C's implicit
1418    // default ctor is the only one and every arg-carrying construction
1419    // fails ("无法将类 m中的构造器 m应用到给定类型; 需要: 没有参数").
1420    // Mirror the nearest ctor-declaring ancestor's public/protected
1421    // ctors as thin `super(..)` delegations.
1422    if !class.is_interface()
1423        && enum_consts.is_none()
1424        && !class.all_methods().any(|m| &*m.name == "<init>")
1425    {
1426        if let Some(mut sup) = class.super_name.clone() {
1427            loop {
1428                if sup == "java/lang/Object" {
1429                    break;
1430                }
1431                let Some(sc) = pool.get(&sup) else { break };
1432                let ctors: Vec<&PoolMethod> = sc
1433                    .all_methods()
1434                    .filter(|m| &*m.name == "<init>")
1435                    .collect();
1436                if !ctors.is_empty() {
1437                    for sm in ctors {
1438                        if sm.access & crate::access::ACC_PRIVATE != 0
1439                            || (sm.access
1440                                & (crate::access::ACC_PUBLIC | crate::access::ACC_PROTECTED)
1441                                == 0)
1442                        {
1443                            continue; // private/package-private: no legal bridge
1444                        }
1445                        let Some(d) = sm.parsed_desc() else { continue };
1446                        let mods = if sm.access & crate::access::ACC_PUBLIC != 0 {
1447                            "public "
1448                        } else {
1449                            "protected "
1450                        };
1451                        if emitted_any {
1452                            out.push('\n');
1453                        }
1454                        out.push_str(&format!("    {}", "    ".repeat(depth)));
1455                        out.push_str(mods);
1456                        out.push_str(&sanitize_ref(&simple));
1457                        out.push('(');
1458                        let mut names = Vec::with_capacity(d.args.len());
1459                        for (i, a) in d.args.iter().enumerate() {
1460                            if i > 0 {
1461                                out.push_str(", ");
1462                            }
1463                            out.push_str(&type_name(pool, a));
1464                            let nm = format!("p{}", i + 1);
1465                            out.push(' ');
1466                            out.push_str(&nm);
1467                            names.push(nm);
1468                        }
1469                        out.push_str(") {\n");
1470                        out.push_str(&format!("    {}    super({});\n", "    ".repeat(depth), names.join(", ")));
1471                        out.push_str(&format!("    {}}}\n", "    ".repeat(depth)));
1472                        emitted_any = true;
1473                    }
1474                    break;
1475                }
1476                match &sc.super_name {
1477                    Some(n) => sup = n.clone(),
1478                    None => break,
1479                }
1480            }
1481        }
1482    }
1483
1484    // Referenced-but-undeclared inherited-ctor bridges. The block above
1485    // covers a class with NO declared ctor; this covers a class that
1486    // DECLARES ctors but is missing one that dex callers reach THROUGH
1487    // it. Dex resolves a `<init>` method-ref up the superclass chain, so
1488    // a subclass `super(2, recv, owner, name, sig, flags)` may target
1489    // `m.<init>(int,Object,Class,String,String,int)` that `m`
1490    // (FunctionReferenceImpl) does NOT declare — its super `l` does.
1491    // Java has no inherited ctors, so without a bridge on `m` every such
1492    // subclass fails ("无法将类 m中的构造器 m应用到给定类型"). Mirror each
1493    // referenced sig C lacks from the nearest ancestor declaring it, as a
1494    // thin `super(..)` delegation. Ref-gated (only sigs actually called
1495    // through C) so ordinary classes gain nothing.
1496    if !class.is_interface()
1497        && enum_consts.is_none()
1498        && class.all_methods().any(|m| &*m.name == "<init>")
1499    {
1500        if let Some(refs) = pool.ctor_ref_sigs(&class.name) {
1501            fn arg_sig(d: &str) -> &str {
1502                let lo = d.find('(').map(|i| i + 1).unwrap_or(0);
1503                let hi = d.find(')').unwrap_or(d.len());
1504                &d[lo..hi]
1505            }
1506            let declared: jdc_core::FxHashSet<&str> = class
1507                .all_methods()
1508                .filter(|m| &*m.name == "<init>")
1509                .map(|m| arg_sig(&m.desc))
1510                .collect();
1511            let mut missing: Vec<&String> = refs
1512                .iter()
1513                .filter(|s| !declared.contains(s.as_str()))
1514                .collect();
1515            missing.sort();
1516            for sig in missing {
1517                // Nearest ancestor declaring a public/protected <init>
1518                // with this exact arg signature.
1519                let mut sup = class.super_name.clone();
1520                let mut found: Option<&PoolMethod> = None;
1521                while let Some(s) = sup {
1522                    if s == "java/lang/Object" {
1523                        break;
1524                    }
1525                    let Some(sc) = pool.get(&s) else { break };
1526                    if let Some(sm) = sc.all_methods().find(|m| {
1527                        &*m.name == "<init>"
1528                            && arg_sig(&m.desc) == sig.as_str()
1529                            && m.access & crate::access::ACC_PRIVATE == 0
1530                            && m.access
1531                                & (crate::access::ACC_PUBLIC | crate::access::ACC_PROTECTED)
1532                                != 0
1533                    }) {
1534                        found = Some(sm);
1535                        break;
1536                    }
1537                    sup = sc.super_name.clone();
1538                }
1539                let Some(sm) = found else {
1540                    // The walk terminated at java/lang/Object — which
1541                    // always declares <init>()V, so an empty-sig ref
1542                    // through this class resolved there dex-wise. The
1543                    // Java implicit default ctor is gone (the class
1544                    // declares other ctors); mirror it as an explicit
1545                    // public bridge (AgentMenuTask$Request →
1546                    // AppBrandProxyUIProcessTask$ProcessRequest:
1547                    // "需要: Parcel 找到: 没有参数", weixin 205).
1548                    if sig.is_empty() {
1549                        if emitted_any {
1550                            out.push('\n');
1551                        }
1552                        out.push_str(&format!("    {}", "    ".repeat(depth)));
1553                        out.push_str("public ");
1554                        out.push_str(&sanitize_ref(&simple));
1555                        out.push_str("() {\n");
1556                        out.push_str(&format!(
1557                            "    {}    super();\n",
1558                            "    ".repeat(depth)
1559                        ));
1560                        out.push_str(&format!("    {}}}\n", "    ".repeat(depth)));
1561                        emitted_any = true;
1562                    }
1563                    continue;
1564                };
1565                let Some(d) = sm.parsed_desc() else { continue };
1566                let mods = if sm.access & crate::access::ACC_PUBLIC != 0 {
1567                    "public "
1568                } else {
1569                    "protected "
1570                };
1571                if emitted_any {
1572                    out.push('\n');
1573                }
1574                out.push_str(&format!("    {}", "    ".repeat(depth)));
1575                out.push_str(mods);
1576                out.push_str(&sanitize_ref(&simple));
1577                out.push('(');
1578                let mut names = Vec::with_capacity(d.args.len());
1579                for (i, a) in d.args.iter().enumerate() {
1580                    if i > 0 {
1581                        out.push_str(", ");
1582                    }
1583                    out.push_str(&type_name(pool, a));
1584                    let nm = format!("p{}", i + 1);
1585                    out.push(' ');
1586                    out.push_str(&nm);
1587                    names.push(nm);
1588                }
1589                out.push_str(") {\n");
1590                out.push_str(&format!(
1591                    "    {}    super({});\n",
1592                    "    ".repeat(depth),
1593                    names.join(", ")
1594                ));
1595                out.push_str(&format!("    {}}}\n", "    ".repeat(depth)));
1596                emitted_any = true;
1597            }
1598        }
1599    }
1600
1601    // Framework-ancestor ctor bridge. The two blocks above mirror POOL
1602    // ancestors; both stop at a framework (off-pool) superclass. A class
1603    // whose DIRECT superclass is a framework type (in android.jar) that
1604    // declares a ctor the class lacks also needs a bridge — `super(args)`
1605    // resolves against the jar. Only the DIRECT-super-is-framework case is
1606    // safe: an intermediate pool ancestor with no matching ctor would
1607    // break the `super()` chain (that ancestor needs its own bridge, which
1608    // its own refs may not trigger). Covers weixin g3 extends
1609    // org.xml.sax.SAXException (`new g3(String)` → SAXException(String)),
1610    // the custom-exception / Parcelable family the pool walk can't reach.
1611    if !class.is_interface() && enum_consts.is_none() {
1612        let direct_fw = class
1613            .super_name
1614            .as_ref()
1615            .map(|s| s != "java/lang/Object" && pool.get(s).is_none())
1616            .unwrap_or(false);
1617        if direct_fw {
1618            if let Some(refs) = pool.ctor_ref_sigs(&class.name) {
1619                fn arg_sig(d: &str) -> &str {
1620                    let lo = d.find('(').map(|i| i + 1).unwrap_or(0);
1621                    let hi = d.find(')').unwrap_or(d.len());
1622                    &d[lo..hi]
1623                }
1624                let declared: jdc_core::FxHashSet<&str> = class
1625                    .all_methods()
1626                    .filter(|m| &*m.name == "<init>")
1627                    .map(|m| arg_sig(&m.desc))
1628                    .collect();
1629                let mut missing: Vec<&String> = refs
1630                    .iter()
1631                    .filter(|s| !declared.contains(s.as_str()))
1632                    .collect();
1633                missing.sort();
1634                for sig in missing {
1635                    // A no-ctor class already gets Java's default
1636                    // `C(){super();}` — never bridge the empty sig for it.
1637                    if sig.is_empty() && declared.is_empty() {
1638                        continue;
1639                    }
1640                    let args = split_arg_descs(sig);
1641                    if emitted_any {
1642                        out.push('\n');
1643                    }
1644                    out.push_str(&format!("    {}", "    ".repeat(depth)));
1645                    out.push_str("public ");
1646                    out.push_str(&sanitize_ref(&simple));
1647                    out.push('(');
1648                    let mut names = Vec::with_capacity(args.len());
1649                    for (i, a) in args.iter().enumerate() {
1650                        if i > 0 {
1651                            out.push_str(", ");
1652                        }
1653                        out.push_str(&type_name(pool, a));
1654                        let nm = format!("p{}", i + 1);
1655                        out.push(' ');
1656                        out.push_str(&nm);
1657                        names.push(nm);
1658                    }
1659                    out.push_str(") {\n");
1660                    out.push_str(&format!(
1661                        "    {}    super({});\n",
1662                        "    ".repeat(depth),
1663                        names.join(", ")
1664                    ));
1665                    out.push_str(&format!("    {}}}\n", "    ".repeat(depth)));
1666                    emitted_any = true;
1667                }
1668            }
1669        }
1670    }
1671
1672    // Missing-abstract-method stubs (R8 tree-shook an interface method a
1673    // concrete class no longer implements; javac rejects the incomplete
1674    // class). Synthesized last so they sit after the real members.
1675    synth_missing_interface_stubs(pool, class, out, depth, &mut emitted_any);
1676
1677    // Fallback enum base: recover the Enum methods the plain-class
1678    // rendering lacks (ordinal() for `switch (x.ordinal())`).
1679    if class.is_enum() && enum_consts.is_none() {
1680        synth_enum_ordinal(pool, class, out, depth, &mut emitted_any);
1681    }
1682
1683    // Static initializer. INTERFACES cannot carry a `static { }` block in
1684    // Java — their clinit only assigns constants, which static_values (or
1685    // the `= null` default) already render as field initializers; skip
1686    // the block entirely.
1687    let skip_clinit = class.is_interface();
1688    if let Some((_, clinit_body)) = enum_consts.take() {
1689        // True-enum <clinit>: the constant assignments are already gone;
1690        // render the remainder (the $VALUES array build) directly.
1691        // Skip an empty remainder (all statements were constant inits).
1692        let empty = match &clinit_body.body {
1693            Stmt::Block(v) => v.iter().all(|s| matches!(s, Stmt::Block(b) if b.is_empty())),
1694            _ => false,
1695        };
1696        if !empty {
1697            if emitted_any {
1698                out.push('\n');
1699            }
1700            out.push_str(&indent(depth + 1));
1701            out.push_str("static {\n");
1702            let p = Printer::new(ctx, &clinit_body.vt);
1703            let text = p.with_indent(depth + 2).into_string(&clinit_body.body);
1704            for line in text.lines() {
1705                if line.trim().is_empty() {
1706                    continue;
1707                }
1708                out.push_str(line);
1709                out.push('\n');
1710            }
1711            out.push_str(&indent(depth + 1));
1712            out.push_str("}\n");
1713            emitted_any = true;
1714        }
1715    } else if let Some(clinit) = (!skip_clinit)
1716        .then(|| class.all_methods().find(|m| &*m.name == "<clinit>"))
1717        .flatten()
1718    {
1719        let mark = out.len();
1720        if emitted_any {
1721            out.push('\n');
1722        }
1723        if emit_method(pool, class, ctx, clinit, depth + 1, false, out)? {
1724            emitted_any = true;
1725        } else {
1726            out.truncate(mark);
1727        }
1728    }
1729
1730    // Nested member classes (clean `$` tails only; anonymous/local/lambda
1731    // classes are emitted as their own top-level files by the driver).
1732    for nested in nested_members(pool, class, ctx) {
1733        if emitted_any {
1734            out.push('\n');
1735        }
1736        let nested_ctx = DexCtx::new(pool, nested);
1737        out.push('\n');
1738        emit_class_body(pool, nested, &nested_ctx, opts, out, depth + 1)?;
1739        emitted_any = true;
1740    }
1741
1742    out.push_str(&ind);
1743    out.push_str("}\n");
1744    Ok(())
1745}
1746
1747/// Member classes of `class` (direct children by the `$` chain / member
1748/// annotations), excluding anonymous / local / lambda shapes.
1749fn nested_members<'a>(
1750    pool: &'a DexPool,
1751    class: &'a PoolClass,
1752    ctx: &DexCtx<'_>,
1753) -> Vec<&'a PoolClass> {
1754    let mut out = Vec::new();
1755    for name in pool.children_of(&class.name) {
1756        let Some(pc) = pool.get(name) else { continue };
1757        // Only a REAL `outer$tail` name is an inline member: the
1758        // children index also carries annotation-derived outers
1759        // (EnclosingClass) with no naming relationship to the child
1760        // (obfuscated apps pair a 1-char name with a long enclosing
1761        // descriptor) — the blind slice panicked there (alipay
1762        // `a.a.a.a.c`, exposed once children_of actually returned data).
1763        let Some(rest) = name
1764            .strip_prefix(class.name.as_str())
1765            .and_then(|t| t.strip_prefix('$'))
1766        else {
1767            continue;
1768        };
1769        if rest.is_empty() || rest.starts_with('-') {
1770            continue;
1771        }
1772        if ctx.find_outer(name).as_deref() != Some(class.name.as_str()) {
1773            continue;
1774        }
1775        let tail = rest.rsplit('$').next().unwrap_or(rest);
1776        if !tail.is_empty() && tail.chars().all(|c| c.is_ascii_digit()) {
1777            continue; // anonymous
1778        }
1779        if tail.starts_with(|c: char| c.is_ascii_digit()) {
1780            continue; // local
1781        }
1782        out.push(pc);
1783    }
1784    out
1785}
1786
1787// Eight parameters are all load-bearing (pool, field, static value,
1788// owner class, output, depth, staticness, interface-init requirement);
1789// bundling them into a struct would obscure each call site.
1790#[allow(clippy::too_many_arguments)]
1791fn emit_field(
1792    pool: &DexPool,
1793    f: &crate::PoolField,
1794    init: Option<&StaticValue>,
1795    f_class: &str,
1796    out: &mut String,
1797    depth: usize,
1798    is_static: bool,
1799    require_init: bool,
1800) {
1801    let ind = indent(depth);
1802    let mut line = String::new();
1803    let a = f.access;
1804    if a & ACC_PUBLIC != 0 || widen_field(f_class, &f.name) {
1805        line.push_str("public ");
1806    } else if a & ACC_PRIVATE != 0 {
1807        line.push_str("private ");
1808    } else if a & ACC_PROTECTED != 0 {
1809        line.push_str("protected ");
1810    }
1811    if is_static {
1812        line.push_str("static ");
1813    }
1814    if a & ACC_FINAL != 0 && !unfinal_field(f_class, &f.name) {
1815        line.push_str("final ");
1816    }
1817    if a & ACC_SYNTHETIC != 0 {
1818        line.push_str("/* synthetic */ ");
1819    }
1820    if a & ACC_TRANSIENT_HINT != 0 {
1821        line.push_str("transient ");
1822    }
1823    if a & ACC_VOLATILE_HINT != 0 {
1824        line.push_str("volatile ");
1825    }
1826    let ty = type_name(pool, &desc_type(&f.desc));
1827    line.push_str(&ty);
1828    line.push(' ');
1829    let fname = jdc_core::rename::field_display(f_class, &f.name, &f.desc).unwrap_or(&*f.name);
1830    line.push_str(&java_ident(fname));
1831    let mut rendered = None;
1832    if let Some(v) = init {
1833        rendered = render_static_value(pool, v, f_class);
1834    }
1835    if rendered.is_none() && require_init {
1836        // Interface fields MUST have an initializer in Java; the dex may
1837        // not carry a static_values entry for a compile-time-constant the
1838        // compiler folded away. Keep it compilable.
1839        let default = match desc_type(&f.desc) {
1840            JavaType::Boolean => "false",
1841            JavaType::Byte | JavaType::Short | JavaType::Char | JavaType::Int => "0",
1842            JavaType::Long => "0L",
1843            JavaType::Float => "0.0F",
1844            JavaType::Double => "0.0",
1845            _ => "null",
1846        };
1847        rendered = Some(default.to_string());
1848    }
1849    if let Some(text) = rendered {
1850        // A long field holding an `Int(i64)` static value needs the `L`
1851        // suffix: without it the literal is an int and overflows
1852        // (`long d = -6343169151696340687` failed javac).
1853        let text = if matches!(desc_type(&f.desc), JavaType::Long)
1854            && text.chars().all(|c| c.is_ascii_digit() || c == '-')
1855        {
1856            format!("{text}L")
1857        } else {
1858            text
1859        };
1860        line.push_str(" = ");
1861        line.push_str(&text);
1862    }
1863    line.push(';');
1864    out.push_str(&ind);
1865    out.push_str(&line);
1866    out.push('\n');
1867}
1868
1869// DEX field access_flags bits 0x40/0x80 are bridge/varargs for METHODS and
1870// volatile(0x40)/transient(0x80) for fields.
1871pub const ACC_VOLATILE_HINT: u32 = 0x40;
1872pub const ACC_TRANSIENT_HINT: u32 = 0x80;
1873
1874fn render_static_value(pool: &DexPool, v: &StaticValue, owner: &str) -> Option<String> {
1875    Some(match v {
1876        StaticValue::Int(i) => i.to_string(),
1877        StaticValue::Float(f) => format_float(*f as f64, true),
1878        StaticValue::Double(d) => format_float(*d, false),
1879        StaticValue::Str(s) => format!("\"{}\"", escape_string(s)),
1880        StaticValue::Type(t) => format!("{}.class", dotted(t)),
1881        StaticValue::Boolean(b) => b.to_string(),
1882        StaticValue::Null => "null".into(),
1883        StaticValue::Field(cls, name) => {
1884            let n = java_ident(name);
1885            if cls == owner {
1886                n.into_owned()
1887            } else {
1888                format!("{}.{}", print_class_name(pool, cls), n)
1889            }
1890        }
1891        StaticValue::Other => return None,
1892    })
1893}
1894
1895/// Render one method straight into the class buffer `out`. Returns
1896/// whether anything was written (skips: no descriptor, deferred to a
1897/// monitored thread). The old shape returned a per-method String that
1898/// the caller copied in — one extra full copy of every method body.
1899/// `enum_promoted`: the class rendered as a true `enum` declaration
1900/// (constants in the header), which changes what a ctor signature may
1901/// declare.
1902fn emit_method(
1903    pool: &DexPool,
1904    class: &PoolClass,
1905    ctx: &DexCtx<'_>,
1906    m: &PoolMethod,
1907    depth: usize,
1908    enum_promoted: bool,
1909    out: &mut String,
1910) -> anyhow::Result<bool> {
1911    let ind = indent(depth);
1912    let desc = m.parsed_desc();
1913
1914    // Signature.
1915    let mut sig = String::with_capacity(192);
1916    let a = m.access;
1917    if a & ACC_PUBLIC != 0 || widen_method(&class.name, &m.name, &m.desc) {
1918        sig.push_str("public ");
1919    } else if a & ACC_PRIVATE != 0 {
1920        // A fallback enum BASE (`/* enum */ class`, not a promoted
1921        // `enum`) renders its constant-specific-body subclasses as
1922        // SEPARATE same-package classes (`j$6 extends j`); their ctors
1923        // call `super(name, ordinal, ..)`, which a PRIVATE base ctor
1924        // forbids ("j(String,int) has private access"). Widen the base
1925        // ctor to package-private so the subclass links. The base is
1926        // abstract → no external instantiation risk. Only the BASE
1927        // (super is Enum/Object), not the constant subclass itself.
1928        let fallback_enum_base_ctor = class.is_enum()
1929            && !enum_promoted
1930            && &*m.name == "<init>"
1931            && class
1932                .super_name
1933                .as_ref()
1934                .map(|s| s == "java/lang/Enum" || s == "java/lang/Object")
1935                .unwrap_or(true);
1936        if !fallback_enum_base_ctor {
1937            sig.push_str("private ");
1938        }
1939    } else if a & ACC_PROTECTED != 0 {
1940        sig.push_str("protected ");
1941    }
1942    let is_clinit = &*m.name == "<clinit>";
1943    let is_init = &*m.name == "<init>";
1944    if a & ACC_STATIC != 0 || is_clinit {
1945        sig.push_str("static ");
1946    }
1947    if a & ACC_FINAL != 0 {
1948        sig.push_str("final ");
1949    }
1950    // `abstract synchronized` is an illegal combination — obfuscated
1951    // builds mark abstract bridges synchronized (WhatsApp
1952    // SQLiteOpenHelper).
1953    if a & (ACC_SYNCHRONIZED | ACC_DECLARED_SYNCHRONIZED) != 0 && a & ACC_ABSTRACT == 0 {
1954        sig.push_str("synchronized ");
1955    }
1956    if a & ACC_NATIVE != 0 {
1957        sig.push_str("native ");
1958    }
1959    if a & ACC_ABSTRACT != 0 {
1960        sig.push_str("abstract ");
1961    }
1962    if a & ACC_SYNTHETIC != 0 {
1963        sig.push_str("/* synthetic */ ");
1964    }
1965
1966    // Body (needed for parameter names even for abstract methods).
1967    let mut body = decompile_method(pool, class, m).ok().flatten();
1968    // An interface method WITH a body is a `default` method (JLS 9.4.3)
1969    // unless static/private — dex carries no `default` flag, so the
1970    // plain form rendered an abstract signature with a body and javac
1971    // rejected every one ("接口抽象方法不能带有主体", weixin 1138). A
1972    // default REQUIRES a body: only push it once the body is confirmed
1973    // (a failed decompile renders a body-less declaration).
1974    if class.is_interface()
1975        && a & (ACC_STATIC | ACC_ABSTRACT | ACC_NATIVE | ACC_PRIVATE | ACC_ANNOTATION) == 0
1976        && body.is_some()
1977    {
1978        sig.insert_str(0, "default ");
1979    }
1980    let param_names: Vec<String> = body
1981        .as_ref()
1982        .map(|b| {
1983            let mut ps: Vec<(u16, String)> =
1984                b.vt.vars
1985                    .iter()
1986                    .filter(|v| v.is_param && v.name != "this")
1987                    .map(|v| (v.slot, v.name.clone()))
1988                    .collect();
1989            ps.sort_by_key(|(s, _)| *s);
1990            ps.into_iter().map(|(_, n)| n).collect()
1991        })
1992        .unwrap_or_else(|| {
1993            (0..desc.as_ref().map(|d| d.args.len()).unwrap_or(0))
1994                .map(|i| format!("p{}", i + 1))
1995                .collect()
1996        });
1997
1998    // Non-static member-inner ctor: the synthetic outer instance rides
1999    // as args[0] (typed as the direct enclosing class). Every emission
2000    // site already passes it implicitly — qualified `outer.new Inner(..)`,
2001    // `this.new Inner(..)` from inside the outer, `super(..)` after
2002    // skip_outer_arg — so the signature drops the param and the body's
2003    // references become `Outer.this`. Without this, every construction
2004    // site fails javac arity ("无法将类…构造器…应用到给定类型" — the
2005    // guava androidx inner-class families, d8 capture lambdas).
2006    let mut inner_arg0 = 0usize;
2007    if is_init {
2008        let tail = class.name.rsplit('$').next().unwrap_or("");
2009        let digit_simple = !tail.is_empty() && tail.chars().all(|c| c.is_ascii_digit());
2010        if let Some(d) = desc.as_ref() {
2011            if let Some(JavaType::Object(outer)) = d.args.first() {
2012                // The direct enclosing class: nesting annotation when
2013                // present, else the `$`-chain parent (find_outer_name —
2014                // d8 lambdas are `Outer$$ExternalSyntheticLambdaN`, where
2015                // a plain rsplit leaves a trailing `$`). It must agree
2016                // with the this$0 type for the rewrite to fire.
2017                let enclosing: Option<String> = class
2018                    .nesting
2019                    .enclosing_class
2020                    .clone()
2021                    .or_else(|| crate::find_outer_name(pool, &class.name));
2022                let direct = enclosing.as_deref() == Some(outer.as_ref());
2023                if direct && !digit_simple && ctx.class_has_this0(&class.name) {
2024                    let param0 = body.as_ref().and_then(|b| {
2025                        b.vt.vars
2026                            .iter()
2027                            .find(|v| v.is_param && v.name != "this")
2028                            .map(|v| v.id)
2029                    });
2030                    let written = match (&body, param0) {
2031                        (Some(b), Some(p0)) => crate::passes::local_is_written(&b.body, p0),
2032                        _ => false,
2033                    };
2034                    if let (Some(p0), false) = (param0, written) {
2035                        if let Some(b) = body.as_mut() {
2036                            // this()-delegations to the SAME class drop the
2037                            // outer arg (all of its ctors share the strip);
2038                            // a super target joins when it is an inner of
2039                            // the same enclosing family.
2040                            let mut eligible: Vec<String> = vec![class.name.clone()];
2041                            if let Some(sup) = &class.super_name {
2042                                let sup_enclosing =
2043                                    crate::find_outer_name(pool, sup);
2044                                if sup_enclosing.as_deref() == Some(outer.as_ref())
2045                                    && ctx.class_has_this0(sup)
2046                                {
2047                                    eligible.push(sup.clone());
2048                                }
2049                            }
2050                            let renamed = crate::apply_class_rename(outer);
2051                            let display = dotted(renamed.as_ref());
2052                            let ty =
2053                                jdc_core::ir::expr::TypeRef::J(JavaType::Object(outer.clone()));
2054                            crate::passes::rewrite_inner_ctor_outer_param(
2055                                &mut b.body,
2056                                p0,
2057                                &display,
2058                                &ty,
2059                                &eligible,
2060                            );
2061                            inner_arg0 = 1;
2062                        }
2063                    }
2064                }
2065            }
2066        }
2067    }
2068
2069    if is_clinit {
2070        // `static { ... }` — the caller strips the method name/params.
2071    } else {
2072        let Some(d) = &desc else { return Ok(false) };
2073        if is_init {
2074            // The ctor name must equal the DECLARED class name of its
2075            // file: flat `$` at depth 0 (own file), own segment when
2076            // inlined in the parent at depth > 0. Case-renamed classes
2077            // use their display name.
2078            let dname = crate::apply_class_rename(&class.name);
2079            let (_, simple) = split_name(&dname);
2080            // emit_method's depth is the METHOD indent = class depth + 1:
2081            // own-file classes (depth 0 header → method depth 1) need the
2082            // flat `$` ctor name; inline nested members use their segment.
2083            let base = if depth <= 1 {
2084                simple.to_string()
2085            } else {
2086                // R8 names can END in `$` (`ThreadMsg$$$`): the last
2087                // `$`-segment is empty — keep the whole simple name.
2088                let seg = simple.rsplit('$').next().unwrap_or(&simple);
2089                if seg.is_empty() {
2090                    simple.to_string()
2091                } else {
2092                    seg.to_string()
2093                }
2094            };
2095            let name = if is_restricted_type_name(&base) {
2096                format!("_{base}")
2097            } else {
2098                base
2099            };
2100            // The ctor name must equal the DECLARED class name — the same
2101            // injective `_u<hex>` sanitizer the header uses (java_ident's
2102            // lossy `-`→`_` fold left `class _u2dDeprecatedOkio` with a
2103            // `private _DeprecatedOkio()` ctor: "方法声明无效; 需要返回
2104            // 类型", which as a PARSE error also masks every semantic
2105            // error in the whole javac run).
2106            sig.push_str(&sanitize_ref(&name));
2107        } else {
2108            sig.push_str(&type_name(pool, &d.ret));
2109            sig.push(' ');
2110            let mname =
2111                jdc_core::rename::field_display(&class.name, &m.name, &m.desc).unwrap_or(&*m.name);
2112            sig.push_str(&java_ident(mname));
2113        }
2114        sig.push('(');
2115        // A promoted enum ctor's dex descriptor carries the compiler-
2116        // synthesized `(String name, int ordinal)` prefix — JLS forbids
2117        // declaring those (they are implicit in the `A(args)` constant
2118        // declarations the promotion emits, and strip_enum_ctor_super
2119        // already removed the `super(name, ordinal, ..)` delegation).
2120        // Skip the pair in the signature, or every constant declaration
2121        // fails javac arity ("无法将枚举…构造器…应用到给定类型"). Only
2122        // when the body never reads the two params — code that genuinely
2123        // uses them keeps the declared form.
2124        let mut arg0 = 0;
2125        if enum_promoted
2126            && is_init
2127            && matches!(d.args.first(), Some(JavaType::Object(s)) if s.as_ref() == "java/lang/String")
2128            && matches!(d.args.get(1), Some(JavaType::Int))
2129        {
2130            // The Kotlin default-arg bridge ctor reads name/ordinal in
2131            // its `this(str, p2, ..)` delegation only — the LEADING pair
2132            // of a this()-delegation drops together with the params, so
2133            // those reads do not block the strip (the constants' extra
2134            // args match the bridge's user params, not the 2-param user
2135            // ctor).
2136            let synthetic: Vec<u32> = body
2137                .as_ref()
2138                .map(|b| {
2139                    b.vt
2140                        .vars
2141                        .iter()
2142                        .filter(|v| v.is_param && v.name != "this")
2143                        .take(2)
2144                        .map(|v| v.id)
2145                        .collect()
2146                })
2147                .unwrap_or_default();
2148            // The delegation-leading extension only applies to the
2149            // Kotlin default-arg BRIDGE: its descriptor ends with
2150            // kotlin/jvm/internal/DefaultConstructorMarker. A REAL user
2151            // ctor whose first two params are (String, int) and merely
2152            // forwards them must keep its signature (weixin +2.9k when
2153            // ungated).
2154            // Marker-name-free bridge shape: (.., int mask, marker).
2155            // R8 renames DefaultConstructorMarker with the rest of the
2156            // stdlib (weixin: kotlin/jvm/internal/i, an empty abstract
2157            // class), so gate on the package + the int-mask/refs pair
2158            // instead of the literal name — a real user enum ctor with
2159            // (int, kotlin/jvm/internal/*) tail params does not occur.
2160            let n_args = d.args.len();
2161            let is_bridge = n_args >= 2
2162                && matches!(&d.args[n_args - 1], JavaType::Object(ref m)
2163                    if m.starts_with("kotlin/jvm/internal/"))
2164                && matches!(d.args[n_args - 2], JavaType::Int);
2165            let mut lead = [0usize, 0usize];
2166            let refs_ok = body.as_ref().is_some_and(|b| {
2167                let uses = crate::passes::count_locals_stmts(std::slice::from_ref(&b.body));
2168                let mut c = b.body.clone();
2169                let cls_name = class.name.as_str();
2170                crate::passes::walk_stmt_exprs(&mut c, &mut |e| {
2171                    if let Expr::Method { name: mn, cls: mc, args, is_special, .. } = e {
2172                        if &**mn == "<init>" && *is_special && mc.as_ref() == cls_name {
2173                            for (k, a) in args.iter().enumerate() {
2174                                if let Expr::Local { var: v, .. } = a {
2175                                    if let Some(pi) = synthetic.iter().position(|p| p == v) {
2176                                        if k == pi && k < 2 {
2177                                            lead[pi] += 1;
2178                                        }
2179                                    }
2180                                }
2181                            }
2182                        }
2183                    }
2184                });
2185                synthetic.iter().enumerate().all(|(pi, id)| {
2186                    let total = uses.get(id).copied().unwrap_or(0);
2187                    if is_bridge {
2188                        total == lead[pi]
2189                    } else {
2190                        total == 0
2191                    }
2192                })
2193            });
2194            if refs_ok {
2195                arg0 = 2;
2196                // Drop the leading (name, ordinal) args of the this()
2197                // delegations.
2198                if let (Some(b), true) = (body.as_mut(), is_bridge) {
2199                    let cls_name = class.name.as_str();
2200                    let syn = synthetic;
2201                    crate::passes::walk_stmt_exprs(&mut b.body, &mut |e| {
2202                        if let Expr::Method { name: mn, cls: mc, args, is_special, .. } = e {
2203                            if &**mn == "<init>" && *is_special && mc.as_ref() == cls_name {
2204                                let mut drop_n = 0;
2205                                for a in args.iter().take(2) {
2206                                    if let Expr::Local { var: v, .. } = a {
2207                                        if syn.contains(v) && drop_n == v - syn[0] {
2208                                            drop_n += 1;
2209                                            continue;
2210                                        }
2211                                    }
2212                                    break;
2213                                }
2214                                for _ in 0..drop_n {
2215                                    args.remove(0);
2216                                }
2217                            }
2218                        }
2219                    });
2220                }
2221            }
2222        }
2223        let n = d.args.len();
2224        let varargs = a & ACC_VARARGS != 0 && n > 0;
2225        let arg0 = arg0 + inner_arg0;
2226        for (i, arg) in d.args.iter().enumerate().skip(arg0) {
2227            if i > arg0 {
2228                sig.push_str(", ");
2229            }
2230            let name = param_names
2231                .get(i)
2232                .cloned()
2233                .unwrap_or_else(|| format!("p{}", i));
2234            if varargs && i + 1 == n {
2235                if let JavaType::Array(inner) = arg {
2236                    sig.push_str(&type_name(pool, inner));
2237                    sig.push_str("...");
2238                } else {
2239                    sig.push_str(&type_name(pool, arg));
2240                }
2241            } else {
2242                sig.push_str(&type_name(pool, arg));
2243            }
2244            sig.push(' ');
2245            // Param names come from dex debug info — obfuscated apps
2246            // name them `_` (reserved since Java 9) or after keywords.
2247            sig.push_str(&java_ident(&name));
2248        }
2249        sig.push(')');
2250    }
2251
2252    if is_clinit {
2253        let Some(b) = body else { return Ok(false) };
2254        // Direct render: the printer starts at the method's ABSOLUTE
2255        // indent and appends into the same buffer that carries the
2256        // header — no intermediate body string, no per-line re-indent
2257        // pass (two full copies of every method body saved).
2258        out.push_str(&ind);
2259        out.push_str("static {\n");
2260        let hdr = out.len();
2261        let printer = Printer::new(ctx, &b.vt)
2262            .with_indent(depth + 1)
2263            .with_output(std::mem::take(out));
2264        let t_print = std::time::Instant::now();
2265        let mut rendered = printer.into_string(&b.body);
2266        crate::method::phase_hit(3, t_print);
2267        if rendered[hdr..].trim().is_empty() {
2268            rendered.truncate(hdr);
2269        }
2270        rendered.push_str(&ind);
2271        rendered.push_str("}\n");
2272        *out = rendered;
2273        return Ok(true);
2274    }
2275    if a & (ACC_ABSTRACT | ACC_NATIVE) != 0 || body.is_none() {
2276        out.push_str(&ind);
2277        out.push_str(&sig);
2278        out.push_str(";\n");
2279        return Ok(true);
2280    }
2281    let Some(b) = body else { return Ok(false) };
2282
2283    // Direct render at the absolute indent level (see the clinit path).
2284    let mut printer = Printer::new(ctx, &b.vt).with_indent(depth + 1);
2285    match &b.desc.ret {
2286        JavaType::Boolean => {
2287            printer = printer.with_ret_bool(true);
2288        }
2289        JavaType::Char => {
2290            printer = printer.with_ret_char(true);
2291        }
2292        JavaType::Byte => {
2293            printer = printer.with_ret_narrow(true, false);
2294        }
2295        JavaType::Short => {
2296            printer = printer.with_ret_narrow(false, true);
2297        }
2298        _ => {}
2299    }
2300    out.push_str(&ind);
2301    out.push_str(&sig);
2302    out.push_str(" {\n");
2303    let hdr = out.len();
2304    let printer = printer.with_output(std::mem::take(out));
2305    let t_print = std::time::Instant::now();
2306    let mut rendered = printer.into_string(&b.body);
2307    crate::method::phase_hit(3, t_print);
2308    if rendered[hdr..].trim().is_empty() {
2309        rendered.truncate(hdr);
2310    }
2311    rendered.push_str(&ind);
2312    rendered.push_str("}\n");
2313    *out = rendered;
2314    Ok(true)
2315}
2316
2317// ---------------------------------------------------------------------------
2318// Naming helpers
2319// ---------------------------------------------------------------------------
2320
2321fn indent(depth: usize) -> String {
2322    "    ".repeat(depth)
2323}
2324
2325/// `$`-separated nesting rendered with dots — but ONLY when every
2326/// segment is a clean Java identifier (a genuine member class chain).
2327/// Anonymous (`Outer$1`), Kotlin synthetic (`Version$bigInteger$2`,
2328/// `...$$inlined$collect$1`) and local-class tails are NOT member
2329/// classes Java can name; the whole name stays flat with `$` (ddc emits
2330/// them as their own top-level files).
2331/// Kotlin emits method names like `invokeSuspend$lambda-0` — `-` (and
2332/// any other non-identifier character) is not legal Java. Deterministic
2333/// mapping, applied identically at declaration and call sites.
2334pub(crate) fn java_ident(name: &str) -> std::borrow::Cow<'_, str> {
2335    // env::var_os is an environ lock+scan — java_ident runs per IDENTIFIER
2336    // (millions per APK), where it profiled as __NSGetEnviron.
2337    static DBG_IDENT: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
2338    if *DBG_IDENT.get_or_init(|| std::env::var_os("DDC_DBG_IDENT").is_some()) {
2339        eprintln!("[ident] {name:?}");
2340    }
2341    let clean = name
2342        .chars()
2343        .all(|c| c.is_ascii_alphanumeric() || c == '_' || c == '$');
2344    // Kotlin names fields/methods `default` (Companion.default) — no Java
2345    // program can declare or reference a keyword; the identical mapping
2346    // lives in jdc-core's call-site sanitizer.
2347    let keyword = matches!(
2348        name,
2349        "abstract"
2350            | "assert"
2351            | "boolean"
2352            | "break"
2353            | "byte"
2354            | "case"
2355            | "catch"
2356            | "char"
2357            | "class"
2358            | "const"
2359            | "continue"
2360            | "default"
2361            | "do"
2362            | "double"
2363            | "else"
2364            | "enum"
2365            | "extends"
2366            | "final"
2367            | "finally"
2368            | "float"
2369            | "for"
2370            | "goto"
2371            | "if"
2372            | "implements"
2373            | "import"
2374            | "instanceof"
2375            | "int"
2376            | "interface"
2377            | "long"
2378            | "native"
2379            | "new"
2380            | "package"
2381            | "private"
2382            | "protected"
2383            | "public"
2384            | "return"
2385            | "short"
2386            | "static"
2387            | "strictfp"
2388            | "super"
2389            | "switch"
2390            | "synchronized"
2391            | "this"
2392            | "throw"
2393            | "throws"
2394            | "transient"
2395            | "try"
2396            | "void"
2397            | "volatile"
2398            | "while"
2399            | "true"
2400            | "false"
2401            | "null"
2402            // `_` is a reserved identifier since Java 9 (Alipay's
2403            // instant-run fields are named `_`) — the `_<name>` mapping
2404            // turns it into `__`, matching jdc-core's call sites.
2405            | "_"
2406    );
2407    // A simple name may not START with a digit either (WhatsApp nests
2408    // `X/0Xx`): the declaration site and every reference (jdc-core's
2409    // sanitize_source_name) prefix the same underscore.
2410    let digit_start = name.chars().next().is_some_and(|c| c.is_ascii_digit());
2411    if clean && !keyword && !digit_start {
2412        std::borrow::Cow::Borrowed(name)
2413    } else if keyword || digit_start {
2414        std::borrow::Cow::Owned(format!("_{name}"))
2415    } else {
2416        // Non-ASCII single chars (Alipay names a field `支`) map to a
2417        // lone `_` — itself reserved since Java 9. Escape it.
2418        let mapped: String = name
2419            .chars()
2420            .map(|c| {
2421                if c.is_ascii_alphanumeric() || c == '_' || c == '$' {
2422                    c
2423                } else {
2424                    '_'
2425                }
2426            })
2427            .collect();
2428        if mapped == "_" {
2429            std::borrow::Cow::Owned("__".to_string())
2430        } else {
2431            std::borrow::Cow::Owned(mapped)
2432        }
2433    }
2434}
2435
2436fn split_name(internal: &str) -> (String, String) {
2437    match internal.rfind('/') {
2438        Some(i) => (internal[..i].to_string(), internal[i + 1..].to_string()),
2439        None => (String::new(), internal.to_string()),
2440    }
2441}
2442
2443/// Dotted source form of an internal name.
2444pub fn dotted(internal: &str) -> String {
2445    let mut out = internal.replace('/', ".");
2446    // `$` → `.` only when the following segment can start a Java
2447    // identifier (anonymous/synthetic tails stay `$`).
2448    let mut i = 0;
2449    while let Some(p) = out[i..].find('$') {
2450        let at = i + p;
2451        // A LEADING `$` (ProGuard keeps `$Gson$Types`) is part of the
2452        // source name — dotting it produced a leading `.Gson.Types`.
2453        let head_ok = at > 0
2454            && out[..at]
2455                .chars()
2456                .next_back()
2457                .is_some_and(|c| c.is_ascii_alphanumeric() || c == '_');
2458        let tail_ok = out[at + 1..]
2459            .chars()
2460            .next()
2461            .is_some_and(|c| c.is_ascii_alphabetic() || c == '_');
2462        if tail_ok && head_ok {
2463            out.replace_range(at..at + 1, ".");
2464            i = at + 1;
2465        } else {
2466            i = at + 1;
2467        }
2468    }
2469    // Every `.`-segment must start an identifier: WhatsApp's `X/0Hl`
2470    // reached `extends` with the digit start unmapped.
2471    sanitize_fq(&out)
2472}
2473
2474fn join_dotted(pool: &DexPool, names: &[String]) -> String {
2475    names
2476        .iter()
2477        .map(|n| print_class_name(pool, n))
2478        .collect::<Vec<_>>()
2479        .join(", ")
2480}
2481
2482/// A printable type name (arrays render with `[]` suffixes). Nested class
2483/// names dot their `$` when the outer chain is known to the pool.
2484/// Split a concatenated method-descriptor ARGUMENT string
2485/// (`"Ljava/lang/String;I[Ljava/lang/Object;"`) into per-parameter
2486/// `JavaType`s. Used by the framework-ancestor ctor bridge, where the
2487/// referenced sig comes from the method-id table (no pool ancestor
2488/// method to read a parsed descriptor from).
2489fn split_arg_descs(sig: &str) -> Vec<JavaType> {
2490    let mut out = Vec::new();
2491    let b = sig.as_bytes();
2492    let mut i = 0usize;
2493    while i < b.len() {
2494        let start = i;
2495        while i < b.len() && b[i] == b'[' {
2496            i += 1;
2497        }
2498        if i < b.len() {
2499            if b[i] == b'L' {
2500                while i < b.len() && b[i] != b';' {
2501                    i += 1;
2502                }
2503                if i < b.len() {
2504                    i += 1; // consume ';'
2505                }
2506            } else {
2507                i += 1; // primitive single char
2508            }
2509        }
2510        if i > start {
2511            out.push(desc_type(&sig[start..i]));
2512        } else {
2513            break; // malformed: avoid an infinite loop
2514        }
2515    }
2516    out
2517}
2518
2519pub fn type_name(pool: &DexPool, t: &JavaType) -> String {
2520    match t {
2521        JavaType::Void => "void".into(),
2522        JavaType::Object(n) => print_class_name(pool, n),
2523        JavaType::Array(inner) => format!("{}[]", type_name(pool, inner)),
2524        other => other.to_java(false),
2525    }
2526}
2527
2528/// `com/foo/Outer$Inner` → `com.foo.Outer.Inner` — each `$` dots only when
2529/// its left side names a known class (literal `$` top-level names survive).
2530// ---------------------------------------------------------------------------
2531// The per-class import map (JLS 6.4.2 obscuring repair).
2532//
2533// A referenced FQN whose FIRST segment equals an in-scope class simple
2534// name binds the qualifier to the CLASS, not the package — `class j3
2535// implements j3.g` is cyclic inheritance and collapses javac's
2536// attribution for the whole package (the Object-cascade root, weixin
2537// 84 files + contagion). The repair: emit `import j3.g;` and render
2538// the SIMPLE name in every type position. The set is populated per
2539// class from a metadata pre-scan (supertypes, field types, method
2540// signature descriptors) and read by print_class_name and the
2541// DexCtx::obscured_simple implementation (which jdc-core's shorten /
2542// java_type_name consult).
2543struct ObscureState {
2544    /// The ROOT class being rendered (its simple name is the obscuring
2545    /// in-scope name).
2546    class: String,
2547    /// Metadata pre-scan results: internal → simple render.
2548    map: jdc_core::FxHashMap<String, String>,
2549    /// Expression-level obscured refs DISCOVERED during the body render
2550    /// (their imports emit at assembly time).
2551    recorded: jdc_core::FxHashSet<String>,
2552    /// Simple names of the CURRENT PACKAGE's own classes: importing a
2553    /// name that collides would SHADOW every same-package use of it in
2554    /// this file (JLS 7.5.1) — those refs stay qualified (obscured,
2555    /// erroring) rather than corrupt.
2556    blocked: jdc_core::FxHashSet<String>,
2557}
2558
2559thread_local! {
2560    static OBSCURE: std::cell::RefCell<Option<ObscureState>> =
2561        const { std::cell::RefCell::new(None) };
2562}
2563
2564/// Install the per-class render state (call at class render entry).
2565pub(crate) fn set_obscured_state(
2566    class: String,
2567    map: jdc_core::FxHashMap<String, String>,
2568    blocked: jdc_core::FxHashSet<String>,
2569) {
2570    OBSCURE.with(|m| {
2571        *m.borrow_mut() = Some(ObscureState {
2572            class,
2573            map,
2574            recorded: jdc_core::FxHashSet::default(),
2575            blocked,
2576        });
2577    });
2578}
2579
2580/// Simple names this class's file renders through the import layer
2581/// (metadata map values + dynamically recorded nested tails). A method
2582/// local with one of these names captures the imported simple name in
2583/// expression position (locals beat single-type imports — `h.e` binds
2584/// a local int `h`, 无法取消引用int); deshadow_locals renames them.
2585/// Empty when no render state is installed (progressive paths).
2586pub(crate) fn obscured_simples_snapshot() -> jdc_core::FxHashSet<String> {
2587    OBSCURE.with(|m| {
2588        let mut out = jdc_core::FxHashSet::default();
2589        let Ok(st) = m.try_borrow() else { return out };
2590        let Some(st) = st.as_ref() else { return out };
2591        for simple in st.map.values() {
2592            out.insert(simple.clone());
2593        }
2594        for internal in &st.recorded {
2595            if let Some(tail) = internal.rsplit(['/', '$']).next() {
2596                if !tail.is_empty() {
2597                    out.insert(tail.to_string());
2598                }
2599            }
2600        }
2601        out
2602    })
2603}
2604
2605/// Take the recorded expression-level refs and clear the state.
2606pub(crate) fn take_recorded_and_clear() -> jdc_core::FxHashSet<String> {
2607    OBSCURE.with(|m| {
2608        m.borrow_mut()
2609            .take()
2610            .map(|st| st.recorded)
2611            .unwrap_or_default()
2612    })
2613}
2614
2615// ---- cross-package access widening ----------------------------------
2616// ART tolerates cross-package access to non-public classes/members that
2617// R8/d8 preserved; Java source cannot express it ("是在不可访问的类或
2618// 接口中定义的", "k在kotlinx.coroutines中不是公共的", "unknownFields 在
2619// l6 中是 protected"). The census scan records every cross-package
2620// reference; the installer keeps the non-public targets and the
2621// declaration renders widen them to public.
2622
2623struct WidenState {
2624    classes: jdc_core::FxHashSet<String>,
2625    methods: jdc_core::FxHashMap<String, jdc_core::FxHashSet<(String, String)>>,
2626    fields: jdc_core::FxHashMap<String, jdc_core::FxHashSet<String>>,
2627    /// (owner, field name) finals written outside the declaring class's
2628    /// ctor/clinit — rendered WITHOUT `final` (see refscan census).
2629    unfinal: jdc_core::FxHashSet<(String, String)>,
2630}
2631
2632static WIDEN: std::sync::OnceLock<WidenState> = std::sync::OnceLock::new();
2633
2634pub(crate) fn install_access_widening(pool: &crate::DexPool) {
2635    let t0 = std::time::Instant::now();
2636    let raw = crate::refscan::access_widening_scan(pool.dexes());
2637    let mut classes: jdc_core::FxHashSet<String> = jdc_core::FxHashSet::default();
2638    for c in &raw.classes {
2639        if let Some(pc) = pool.get_if_materialized(c) {
2640            if pc.access & crate::access::ACC_PUBLIC == 0 {
2641                classes.insert(c.clone());
2642            }
2643        }
2644    }
2645    let mut methods: jdc_core::FxHashMap<String, jdc_core::FxHashSet<(String, String)>> =
2646        jdc_core::FxHashMap::default();
2647    for (owner, set) in &raw.methods {
2648        let Some(pc) = pool.get_if_materialized(owner) else {
2649            continue;
2650        };
2651        let is_enum = pc.access & crate::access::ACC_ENUM != 0;
2652        let mut keep: jdc_core::FxHashSet<(String, String)> =
2653            jdc_core::FxHashSet::default();
2654        for (name, desc) in set {
2655            // Enum ctors are source-level private-only; clinit is never
2656            // referenced. Widening either is a javac "modifier not
2657            // allowed here".
2658            if name == "<clinit>" || (is_enum && name == "<init>") {
2659                continue;
2660            }
2661            let nonpub = pc
2662                .all_methods()
2663                .any(|m| &*m.name == name.as_str() && &*m.desc == desc.as_str()
2664                    && m.access & crate::access::ACC_PUBLIC == 0);
2665            if nonpub {
2666                keep.insert((name.clone(), desc.clone()));
2667            }
2668        }
2669        if !keep.is_empty() {
2670            methods.insert(owner.clone(), keep);
2671        }
2672    }
2673    // Override-closure DOWN the hierarchy: a widened (public) super
2674    // method whose subclass override stays protected/package is
2675    // "无法覆盖...更低的访问权限" (lark +823). Every transitive
2676    // subclass declaring the same (name, desc) non-public widens too.
2677    {
2678        let mut subs: jdc_core::FxHashMap<String, Vec<String>> =
2679            jdc_core::FxHashMap::default();
2680        for name in &pool.order {
2681            let Some(pc) = pool.get_if_materialized(name) else {
2682                continue;
2683            };
2684            if let Some(sup) = &pc.super_name {
2685                subs.entry(sup.clone()).or_default().push(name.clone());
2686            }
2687            for i in &pc.interfaces {
2688                subs.entry(i.clone()).or_default().push(name.clone());
2689            }
2690        }
2691        let seeds: Vec<(String, String, String)> = methods
2692            .iter()
2693            .flat_map(|(o, set)| {
2694                set.iter().map(|(n, d)| (o.clone(), n.clone(), d.clone()))
2695            })
2696            .collect();
2697        for (owner, mname, mdesc) in seeds {
2698            let mut queue: Vec<String> = match subs.get(&owner) {
2699                Some(v) => v.clone(),
2700                None => continue,
2701            };
2702            let mut seen: jdc_core::FxHashSet<String> = jdc_core::FxHashSet::default();
2703            while let Some(c) = queue.pop() {
2704                if !seen.insert(c.clone()) {
2705                    continue;
2706                }
2707                let Some(pc) = pool.get_if_materialized(&c) else {
2708                    continue;
2709                };
2710                let declares_nonpub = pc.all_methods().any(|m| {
2711                    &*m.name == mname.as_str()
2712                        && &*m.desc == mdesc.as_str()
2713                        && m.access & crate::access::ACC_PUBLIC == 0
2714                });
2715                if declares_nonpub {
2716                    methods
2717                        .entry(c.clone())
2718                        .or_default()
2719                        .insert((mname.clone(), mdesc.clone()));
2720                }
2721                if let Some(v) = subs.get(&c) {
2722                    queue.extend(v.iter().cloned());
2723                }
2724            }
2725        }
2726        // Subclass `super(..)` targets. A class C extends S; C's ctor
2727        // renders `super(..)` against S's ctor. When R8 INLINES S's
2728        // trivial ctor, C's dex ctor calls `Object.<init>` directly (not
2729        // `S.<init>`), so the bytecode census above never sees the S.<init>
2730        // reference and a PRIVATE S ctor stays private — the rendered
2731        // `super()` then fails ("R() 在 R 中是 private 访问控制", lark ×122:
2732        // pl.droidsonroids.gif.R extends com.ss.android.lark.R, whose ctor
2733        // R8 inlined to Object.<init>). Widen every private ctor of a
2734        // class that HAS subclasses so the implicit super() links. Enums
2735        // are excluded (their ctors are source-level private-only).
2736        for sup in subs.keys() {
2737            let Some(pc) = pool.get_if_materialized(sup) else {
2738                continue;
2739            };
2740            if pc.access & crate::access::ACC_ENUM != 0 {
2741                continue;
2742            }
2743            for m in pc.all_methods() {
2744                if &*m.name == "<init>" && m.access & crate::access::ACC_PRIVATE != 0 {
2745                    methods
2746                        .entry(sup.clone())
2747                        .or_default()
2748                        .insert((m.name.to_string(), m.desc.to_string()));
2749                }
2750            }
2751        }
2752    }
2753    let mut fields: jdc_core::FxHashMap<String, jdc_core::FxHashSet<String>> =
2754        jdc_core::FxHashMap::default();
2755    for (owner, set) in &raw.fields {
2756        let Some(pc) = pool.get_if_materialized(owner) else {
2757            continue;
2758        };
2759        let keep: jdc_core::FxHashSet<String> = set
2760            .iter()
2761            .filter(|name| {
2762                pc.static_fields
2763                    .iter()
2764                    .chain(pc.instance_fields.iter())
2765                    .any(|f| f.name == name.as_str()
2766                        && f.access & crate::access::ACC_PUBLIC == 0)
2767            })
2768            .cloned()
2769            .collect();
2770        if !keep.is_empty() {
2771            fields.insert(owner.clone(), keep);
2772        }
2773    }
2774    if std::env::var("DDC_STATS").is_ok() {
2775        eprintln!(
2776            "[renames] access widening: classes={} method-owners={} field-owners={} scan={:?}",
2777            classes.len(),
2778            methods.len(),
2779            fields.len(),
2780            t0.elapsed()
2781        );
2782    }
2783
2784    let unfinal: jdc_core::FxHashSet<(String, String)> =
2785        raw.unfinal_fields.into_iter().collect();
2786    let _ = WIDEN.set(WidenState { classes, methods, fields, unfinal });
2787}
2788
2789#[inline]
2790pub(crate) fn widen_class(internal: &str) -> bool {
2791    WIDEN.get().is_some_and(|w| w.classes.contains(internal))
2792}
2793
2794#[inline]
2795pub(crate) fn widen_method(owner: &str, name: &str, desc: &str) -> bool {
2796    WIDEN.get().is_some_and(|w| {
2797        w.methods
2798            .get(owner)
2799            .is_some_and(|s| s.contains(&(name.to_string(), desc.to_string())))
2800    })
2801}
2802
2803#[inline]
2804pub(crate) fn widen_field(owner: &str, name: &str) -> bool {
2805    WIDEN.get().is_some_and(|w| {
2806        w.fields.get(owner).is_some_and(|s| s.contains(name))
2807    })
2808}
2809
2810/// True when the census saw this final field written outside the
2811/// declaring class's `<init>`/`<clinit>` — the `final` modifier must
2812/// not render ("无法为 final 变量 v 分配值" at every foreign write).
2813#[inline]
2814pub(crate) fn unfinal_field(owner: &str, name: &str) -> bool {
2815    WIDEN.get().is_some_and(|w| {
2816        w.unfinal.contains(&(owner.to_string(), name.to_string()))
2817    })
2818}
2819
2820/// Render-time lookup: the metadata map first; otherwise an
2821/// expression-level ref whose first segment equals the root class's
2822/// simple name — record it (its import emits at assembly) and render
2823/// the simple name NOW.
2824pub(crate) fn obscured_render_pub(internal: &str) -> Option<String> {
2825    OBSCURE.with(|m| {
2826        let mut st = m.borrow_mut();
2827        let st = st.as_mut()?;
2828        if let Some(simple) = st.map.get(internal) {
2829            return Some(simple.clone());
2830        }
2831        let first = internal.split('/').next().unwrap_or("");
2832        let own = st.class.rsplit('/').next().unwrap_or("");
2833        if first == own && internal.split('/').count() >= 2 {
2834            // A NESTED internal's in-scope simple name is its `$` tail
2835            // (`x/a$b` imports/renders as `b`) — the slash-tail left the
2836            // `$` in the render (`t2$a` flat against a nested emission).
2837            let simple = internal
2838                .rsplit(['/', '$'])
2839                .next()
2840                .unwrap_or("")
2841                .to_string();
2842            if !simple.is_empty() && !st.blocked.contains(&simple) {
2843                st.recorded.insert(internal.to_string());
2844                return Some(simple);
2845            }
2846        }
2847        None
2848    })
2849}
2850
2851/// The pre-scan: internal names referenced by the class's metadata
2852/// (supertypes, field types, method descriptors) whose first segment
2853/// equals the class's own simple name and which exist in the pool —
2854/// these get imports and simple-name renders.
2855fn compute_obscured_renders(pool: &DexPool, class: &PoolClass) -> jdc_core::FxHashMap<String, String> {
2856    let own_simple = class.name.rsplit('/').next().unwrap_or("");
2857    if own_simple.is_empty() {
2858        return jdc_core::FxHashMap::default();
2859    }
2860    let mut refs: jdc_core::FxHashSet<String> = jdc_core::FxHashSet::default();
2861    for sup in class.interfaces.iter() {
2862        refs.insert(sup.clone());
2863    }
2864    if let Some(sup) = &class.super_name {
2865        refs.insert(sup.clone());
2866    }
2867    for f in class.static_fields.iter().chain(class.instance_fields.iter()) {
2868        if let JavaType::Object(n) = crate::desc_type(&f.desc) {
2869            refs.insert(n.to_string());
2870        }
2871    }
2872    for m in class.all_methods() {
2873        if let Some(d) = m.parsed_desc() {
2874            for a in d.args.iter() {
2875                if let JavaType::Object(n) = a {
2876                    refs.insert(n.to_string());
2877                }
2878            }
2879            if let JavaType::Object(n) = &d.ret {
2880                refs.insert(n.to_string());
2881            }
2882        }
2883    }
2884    let mut out: jdc_core::FxHashMap<String, String> = jdc_core::FxHashMap::default();
2885    for r in refs {
2886        // First segment == the class's own simple name: the qualified
2887        // render would be obscured. The name must exist in the pool for
2888        // the import to resolve. The KEY stays internal (renders look
2889        // refs up by internal name), but the import line and the
2890        // simple name use the RENAMED display — a collision-renamed
2891        // class renders under its display name and an import of the
2892        // internal name does not resolve.
2893        let first = r.split('/').next().unwrap_or("");
2894        if first == own_simple && r.split('/').count() >= 2 && pool.get(&r).is_some() {
2895            // Own-family refs (the class itself, its nested members)
2896            // render through member scope — an import would be redundant
2897            // (self-import) or shadow a same-package sibling named like
2898            // the tail (`import t.t2.a` hijacks every bare `a` that
2899            // meant sibling class t.a — lark t/t2).
2900            if r == class.name || r.starts_with(&format!("{}$", class.name)) {
2901                continue;
2902            }
2903            let display = crate::apply_class_rename(&r);
2904            let simple = display
2905                .rsplit(['/', '$'])
2906                .next()
2907                .unwrap_or("")
2908                .to_string();
2909            if !simple.is_empty() {
2910                out.insert(r.clone(), simple);
2911            }
2912        }
2913    }
2914    out
2915}
2916
2917pub fn print_class_name(pool: &DexPool, internal: &str) -> String {
2918    if let Some(simple) = obscured_render_pub(internal) {
2919        return sanitize_ref(&simple);
2920    }
2921    let cow = crate::apply_class_rename(internal);
2922    let internal: &str = &cow;
2923    // Flat EMISSION UNITS: a digit-tail member (anonymous / d8-lambda
2924    // shape, `Outer$lruCache$1`) is emitted as its own top-level file
2925    // whose simple name keeps every `$` — the `$` boundaries inside
2926    // that unit are part of the NAME, not nesting. References must use
2927    // the flat unit name; the generic loop below dotted the first
2928    // boundary (`LruCacheKt.lruCache$1`) against the declaration
2929    // `class LruCacheKt$lruCache$1` — every use of the type failed and
2930    // javac's attribution for the whole file collapsed (3627 pure-
2931    // cascade files on weibo).
2932    if internal.contains('$') && pool.get(internal).is_some() {
2933        if let Some(root) = emission_root(pool, internal) {
2934            if root.contains('$') {
2935                let below = &internal[root.len()..];
2936                if below.is_empty() {
2937                    // The unit itself: the flat name IS the reference.
2938                    return sanitize_ref(&internal.replace('/', "."));
2939                }
2940                let segs: Vec<&str> = below[1..].split('$').collect();
2941                if segs.iter().any(|s| s.is_empty()) {
2942                    // R8 tails like `ThreadMsg$$$` cannot be dotted at
2943                    // all — keep the whole name flat.
2944                    return sanitize_ref(&internal.replace('/', "."));
2945                }
2946                let mut out = root.replace('/', ".");
2947                for s in segs {
2948                    out.push('.');
2949                    out.push_str(s);
2950                }
2951                return sanitize_ref(&out);
2952            }
2953        }
2954    }
2955    let mut out = String::new();
2956    // `known` must test the ACCUMULATED internal prefix, not the bare
2957    // inter-`$` segment: the per-segment shape checked `pool.get("a")`
2958    // for the second level of `s5/o$a$b`, missed, and rendered the
2959    // undeclarable reference `s5.o.a$b` (找不到符号) for every nested-
2960    // nested type.
2961    let mut off = 0usize;
2962    loop {
2963        let rest = &internal[off..];
2964        match rest.find('$') {
2965            Some(i) => {
2966                let seg = &rest[..i];
2967                let prefix = &internal[..off + i];
2968                let known = pool.get(prefix).is_some()
2969                    || jdc_core::rename::is_renamed_display(prefix)
2970                    // The FULL name is not a pool class: this `$` cannot
2971                    // be a literal name (pool literal classes — an app's
2972                    // own `View$OnUnhandledKeyEventListener` — keep their
2973                    // `$` here AND at their declaration), so it can only
2974                    // be an external framework nesting boundary
2975                    // (`View$OnClickListener` → `.OnClickListener`).
2976                    || !pool.get(internal).is_some();
2977                // The `$` may only become a nesting dot when the tail
2978                // segment STARTS a Java identifier: R8's desugared-
2979                // library names carry `$` inside PACKAGE paths
2980                // (`j$/util/...` dotted into `j..util`) and suffixes
2981                // like `Collection$-EL` or anonymous `RequestId$1`
2982                // cannot be dotted under any reading.
2983                let tail_ok = rest[i + 1..]
2984                    .chars()
2985                    .next()
2986                    .is_some_and(|c| c.is_ascii_alphabetic() || c == '_');
2987                out.push_str(&seg.replace('/', "."));
2988                out.push_str(if known && tail_ok { "." } else { "$" });
2989                off += i + 1;
2990            }
2991            None => {
2992                out.push_str(&rest.replace('/', "."));
2993                return sanitize_ref(&out);
2994            }
2995        }
2996    }
2997}
2998
2999/// The top-level EMISSION UNIT ancestor of a pool class: walking the
3000/// `$`-chain upward (outer_of), the first ancestor that is itself
3001/// emitted as its own compilation unit — a digit-tail rest marks a
3002/// flat boundary (see top_level_classes). Clean members render inline
3003/// in their outer, so only the unit's own name may carry `$`.
3004fn emission_root(pool: &DexPool, internal: &str) -> Option<String> {
3005    let mut cur = internal.to_string();
3006    loop {
3007        let outer = pool.outer_of(&cur)?;
3008        let rest = cur
3009            .strip_prefix(outer)
3010            .and_then(|t| t.strip_prefix('$'))
3011            .unwrap_or("");
3012        if !crate::clean_member_tail(rest) {
3013            return Some(cur);
3014        }
3015        cur = outer.to_string();
3016    }
3017}
3018
3019/// Class-file names may contain characters Java source identifiers
3020/// cannot (`Collection$-EL`); the deterministic mapping matches the
3021/// declaration sites (java_ident).
3022/// Every `.`-segment of a fully-qualified name must start a Java
3023/// identifier: obfuscators emit `package do;` and `..badge.new..` paths.
3024///
3025/// PUBLIC on purpose: this is the single source of truth for the
3026/// declaration↔file-name mapping. The CLI writer used to keep its own
3027/// copy (`sanitize_file_seg`) which had drifted — it lacked the lone-`_`
3028/// escape below — so `l.֡` declared `class __` inside a file named
3029/// `_.java`. Callers reach it through `sanitize_seg`/`sanitize_internal`.
3030///
3031/// INJECTIVE on the characters, which is the property the writer needs.
3032/// The old mapping folded every non-identifier character to `_`, so
3033/// `l.᩻ܶ`, `l.᩻ۡ` and `l.֫᩷` all became `l.__`: on bin.mt.plus 22,636
3034/// distinct classes in package `l` collapsed onto three file names, the
3035/// writer's EEXIST branch overwrote them, and 22,633 sources were
3036/// destroyed with exit code 0. `_u<hex>` keeps them distinct, is
3037/// self-describing (it encodes the original code point) and costs the
3038/// decompiler nothing — the alternative, minting 22,636 registry
3039/// renames, makes every type reference allocate and took this sample
3040/// from 5.2 s to 26.8 s.
3041///
3042/// The only residual collisions are *lookalikes*: a literal class named
3043/// `_u1a7b` beside the class `᩻` (same code point), or the pre-existing
3044/// keyword form `_do` beside `do`. Those are detected and repaired
3045/// deterministically by `lossy_sanitize_renames`, which is why that pass
3046/// still exists.
3047pub fn sanitize_fq(dotted: &str) -> String {
3048    dotted
3049        .split('.')
3050        .map(sanitize_fq_seg)
3051        .collect::<Vec<_>>()
3052        .join(".")
3053}
3054
3055/// One `.`-segment: escape, then repair identifier-position problems.
3056fn sanitize_fq_seg(seg: &str) -> String {
3057    // Ident-safe ASCII passes through unchanged (the overwhelmingly
3058    // common case, and the only one on the borrow-fast path); every
3059    // other character — non-ASCII, and ASCII punctuation such as the
3060    // `-` in `Collection$-EL` — becomes `_u<hex>`.
3061    let plain = seg
3062        .chars()
3063        .all(|c| c.is_ascii() && (c.is_ascii_alphanumeric() || c == '_' || c == '$'));
3064    let mut out = if plain {
3065        seg.to_string()
3066    } else {
3067        use std::fmt::Write as _;
3068        let mut s = String::with_capacity(seg.len() + 8);
3069        for c in seg.chars() {
3070            if c.is_ascii() && (c.is_ascii_alphanumeric() || c == '_' || c == '$') {
3071                s.push(c);
3072            } else {
3073                s.push_str("_u");
3074                let _ = write!(s, "{:x}", c as u32);
3075            }
3076        }
3077        s
3078    };
3079    // `out` is pure ASCII from here: the identifier-position repairs.
3080    if is_java_keyword_name(&out)
3081        || is_restricted_type_name(&out)
3082        || out.chars().next().is_some_and(|c| c.is_ascii_digit())
3083    {
3084        out.insert(0, '_');
3085    }
3086    // A lone `_` is a reserved IDENTIFIER since Java 9.
3087    if out == "_" {
3088        out = "__".to_string();
3089    }
3090    out
3091}
3092
3093/// Restricted contextual TYPE names — legal as member/local names
3094/// (rt.jar compiles `var` locals), illegal in class declarations and
3095/// type references. Consulted only on CLASS-name paths.
3096pub(crate) fn is_restricted_type_name(s: &str) -> bool {
3097    matches!(s, "var" | "yield" | "record" | "sealed" | "permits")
3098}
3099
3100fn is_java_keyword_name(s: &str) -> bool {
3101    matches!(
3102        s,
3103        "abstract"
3104            | "assert"
3105            | "boolean"
3106            | "break"
3107            | "byte"
3108            | "case"
3109            | "catch"
3110            | "char"
3111            | "class"
3112            | "const"
3113            | "continue"
3114            | "default"
3115            | "do"
3116            | "double"
3117            | "else"
3118            | "enum"
3119            | "extends"
3120            | "final"
3121            | "finally"
3122            | "float"
3123            | "for"
3124            | "goto"
3125            | "if"
3126            | "implements"
3127            | "import"
3128            | "instanceof"
3129            | "int"
3130            | "interface"
3131            | "long"
3132            | "native"
3133            | "new"
3134            | "package"
3135            | "private"
3136            | "protected"
3137            | "public"
3138            | "return"
3139            | "short"
3140            | "static"
3141            | "strictfp"
3142            | "super"
3143            | "switch"
3144            | "synchronized"
3145            | "this"
3146            | "throw"
3147            | "throws"
3148            | "transient"
3149            | "try"
3150            | "void"
3151            | "volatile"
3152            | "while"
3153            | "true"
3154            | "false"
3155            | "null"
3156            // `_` is a reserved identifier since Java 9 (Alipay's
3157            // instant-run fields are named `_`) — the `_<name>` mapping
3158            // turns it into `__`, matching jdc-core's call sites.
3159            | "_"
3160    )
3161}
3162
3163fn sanitize_ref(name: &str) -> String {
3164    sanitize_fq(name)
3165}
3166
3167/// Dotted source form of an internal name.
3168pub fn dotted_pool(pool: &DexPool, internal: &str) -> String {
3169    print_class_name(pool, internal)
3170}
3171
3172/// Unused import silencer.
3173#[allow(dead_code)]
3174fn _unused(_: &dyn Fn(&JavaType) -> jdc_core::types::GenericType) {
3175    let _ = java_type_to_generic;
3176}
3177
3178#[cfg(test)]
3179mod sanitize_tests {
3180    use super::sanitize_fq;
3181
3182    /// The property the file writer depends on: distinct class names must
3183    /// reach distinct output paths. The lossy predecessor of this mapping
3184    /// folded every non-identifier character to `_`, so 22,636 classes of
3185    /// `bin.mt.plus` (names built from Thai/Yi/Syriac code points) became
3186    /// three (`l.__`, `l.___`, `l._`) and all but three were overwritten.
3187    #[test]
3188    fn distinct_obfuscated_names_stay_distinct() {
3189        // The three names that collapsed onto `l/__.java` in the field.
3190        let names = ["l.᩻ܶ", "l.᩻ۡ", "l.֫᩷", "l.֡", "l.᩻᩶ۛ"];
3191        let mut mapped: Vec<String> = names.iter().map(|n| sanitize_fq(n)).collect();
3192        let before = mapped.len();
3193        mapped.sort();
3194        mapped.dedup();
3195        assert_eq!(mapped.len(), before, "sanitizer folded distinct names: {mapped:?}");
3196        assert!(
3197            mapped.iter().all(|m| m.is_ascii()),
3198            "sanitizer must stay ASCII: {mapped:?}"
3199        );
3200        // Self-describing: the escape carries the original code point.
3201        assert_eq!(sanitize_fq("l.᩻ܶ"), "l._u1a7b_u736");
3202    }
3203
3204    /// Every output must be a legal Java identifier segment: no leading
3205    /// digit, no keyword, not the lone `_` reserved since Java 9.
3206    #[test]
3207    fn output_is_a_legal_identifier() {
3208        for (input, want) in [
3209            ("do", "_do"),
3210            ("_", "__"),
3211            ("0Xx", "_0Xx"),
3212            ("Collection$-EL", "Collection$_u2dEL"),
3213            ("᩻", "_u1a7b"),
3214            ("a", "a"),
3215            ("A$B", "A$B"),
3216            ("x_y", "x_y"),
3217        ] {
3218            assert_eq!(sanitize_fq(input), want, "input {input:?}");
3219        }
3220    }
3221
3222    /// Documented residual: ident-safe ASCII passes through unchanged, so a
3223    /// literal `_u1a7b` collides with the single character U+1A7B. The
3224    /// collision is real — which is why `lossy_sanitize_renames` still owns
3225    /// a repair pass — and this test pins the shape the guard must catch.
3226    #[test]
3227    fn lookalike_collision_is_known_and_bounded() {
3228        assert_eq!(sanitize_fq("_u1a7b"), sanitize_fq("᩻"));
3229    }
3230}