dotscope 0.9.1

A high-performance, cross-platform framework for analyzing and reverse engineering .NET PE executables
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
//! Exception handling utilities for the emulation engine.
//!
//! This module contains the exception handling support functions used by the
//! [`EmulationController`](super::controller::EmulationController)'s execution
//! loop. It implements the two-pass exception handling model from ECMA-335
//! §III.3.47 (throw) and §12.4.2 (exception handling).
//!
//! # Functions
//!
//! - [`route_clr_exception`] — Top-level exception routing: searches the
//!   current method for a handler, then unwinds the call stack frame by
//!   frame until a matching handler is found or the exception is unhandled.
//! - [`find_exception_handler`] — Searches a single method's exception
//!   handling table for a `catch`/`filter`/`finally`/`fault` clause that
//!   covers the current instruction offset and matches the exception type.
//! - [`schedule_finally_blocks`] — Schedules `finally` blocks for execution
//!   when a `leave` instruction exits a protected region.
//! - [`create_clr_exception`] / [`create_exception_from_type`] — Allocate
//!   synthetic CLR exception objects on the managed heap.
//! - [`wrap_in_target_invocation_exception`] — Wraps an exception in a
//!   `TargetInvocationException`, mirroring reflection invoke semantics.
//! - [`resolve_exception_type`] — Determines the type token of a thrown
//!   exception value by inspecting its `EmValue` kind and heap metadata.
//! - [`apply_handler_match`] — Transfers control to a matched handler,
//!   pushing the exception onto the evaluation stack for `catch`/`filter`.
//! - [`track_cctor_failure_if_needed`] — Records `.cctor` failures in the
//!   [`CctorTracker`] so subsequent type access re-throws the stored exception.
//!
//! # Handler Search Order
//!
//! Exception handlers are searched in the order they appear in the method's
//! exception handling table (innermost-first per ECMA-335). For each clause:
//!
//! 1. Check if the clause's `try` region covers the current offset
//! 2. For `catch` — verify type compatibility via [`EmulationContext::is_type_compatible`]
//! 3. For `filter` — begin filter code execution (the main loop evaluates it)
//! 4. For `finally`/`fault` — schedule for later execution during unwinding
//!
//! # Stack Unwinding
//!
//! When no handler is found in the current method, the controller pops
//! frames from the call stack. At each frame boundary, if the frame was
//! entered via reflection invoke, the exception is wrapped in a
//! `TargetInvocationException` per real .NET behavior.

use log::{debug, trace};

use crate::{
    emulation::{
        engine::{
            cctors::CctorTracker, context::EmulationContext, error::synthetic_exception,
            interpreter::Interpreter, EmulationError,
        },
        exception::{
            ExceptionClause, ExceptionInfo, HandlerMatch, InstructionLocation, ThreadExceptionState,
        },
        memory::AddressSpace,
        thread::EmulationThread,
        tokens, EmValue,
    },
    metadata::token::Token,
    Result,
};

/// Creates a synthetic CLR exception object from an [`EmulationError`].
///
/// Maps the error to its corresponding BCL exception type token via
/// [`EmulationError::to_exception_token`] and allocates a heap object.
///
/// # Arguments
///
/// * `address_space` — The shared address space for heap allocation.
/// * `error` — The emulation error to convert into an exception.
///
/// # Returns
///
/// An `EmValue::ObjectRef` pointing to the newly allocated exception.
///
/// # Errors
///
/// Returns an error if heap allocation fails.
pub fn create_clr_exception(
    address_space: &AddressSpace,
    error: &EmulationError,
) -> Result<EmValue> {
    let type_token = error.to_exception_token();
    let heap_ref = address_space.alloc_object(type_token)?;
    Ok(EmValue::ObjectRef(heap_ref))
}

/// Creates a synthetic CLR exception object from an exception type token.
///
/// Allocates a heap object representing a CLR exception of the given type.
/// Used when hooks or instructions need to throw a specific exception type
/// (e.g., `InvalidOperationException` from a delegate dispatch failure).
///
/// # Arguments
///
/// * `address_space` — The shared address space for heap allocation.
/// * `exception_type` — The synthetic exception type token (e.g.,
///   `synthetic_exception::INVALID_OPERATION`).
///
/// # Returns
///
/// An `EmValue::ObjectRef` pointing to the newly allocated exception.
///
/// # Errors
///
/// Returns an error if heap allocation fails.
pub fn create_exception_from_type(
    address_space: &AddressSpace,
    exception_type: Token,
) -> Result<EmValue> {
    let heap_ref = address_space.alloc_object(exception_type)?;
    Ok(EmValue::ObjectRef(heap_ref))
}

/// Wraps an exception in a `TargetInvocationException`, mirroring real .NET
/// behavior where `MethodBase.Invoke` wraps all target exceptions.
///
/// Creates a new TIE heap object and sets its `InnerException` field to the
/// original exception so that `Exception.get_InnerException` returns it.
///
/// # Arguments
///
/// * `address_space` — The shared address space for heap allocation.
/// * `inner_exception` — The original exception to wrap.
///
/// # Returns
///
/// A tuple `(EmValue::ObjectRef, Token)` containing the TIE object and its
/// type token.
///
/// # Errors
///
/// Returns an error if heap allocation fails.
pub fn wrap_in_target_invocation_exception(
    address_space: &AddressSpace,
    inner_exception: &EmValue,
) -> Result<(EmValue, Token)> {
    let tie_type = synthetic_exception::TARGET_INVOCATION;
    let tie_ref = address_space.alloc_object(tie_type)?;
    // Set InnerException field on the TIE object
    address_space.set_field(
        tie_ref,
        tokens::exception_fields::INNER_EXCEPTION,
        inner_exception.clone(),
    )?;
    Ok((EmValue::ObjectRef(tie_ref), tie_type))
}

/// Routes a CLR exception through CIL exception handling (`catch`/`finally`/`fault`).
///
/// Creates the exception state, searches for a handler in the current method, and
/// if not found, unwinds the call stack looking for handlers in callers.
///
/// # Arguments
///
/// * `address_space` — Shared address space for allocating wrapper exceptions.
/// * `context` — Assembly metadata context for method/type lookup.
/// * `interpreter` — Updated to the handler's entry point if one is found.
/// * `thread` — The emulation thread (frames popped during unwinding).
/// * `current_method` — Token of the method where the exception was thrown.
/// * `exception_type` — Resolved type token of the exception.
/// * `exception` — The exception value (`EmValue::ObjectRef` or other).
///
/// # Returns
///
/// * `Ok(true)` — A handler was found and control was transferred.
/// * `Ok(false)` — The exception is unhandled (caller should return
///   [`EmulationOutcome::UnhandledException`](super::EmulationOutcome::UnhandledException)).
///
/// # Errors
///
/// Returns an error if handler search or heap allocation fails.
pub fn route_clr_exception(
    address_space: &AddressSpace,
    context: &EmulationContext,
    interpreter: &mut Interpreter,
    thread: &mut EmulationThread,
    current_method: Token,
    exception_type: Token,
    exception: EmValue,
) -> Result<bool> {
    let current_offset = interpreter.ip().offset();
    let throw_location = InstructionLocation::new(current_method, current_offset);

    debug!(
        "route_clr_exception: method=0x{:08X} offset=0x{:04X} exc_type=0x{:08X} depth={}",
        current_method.value(),
        current_offset,
        exception_type.value(),
        thread.call_depth()
    );

    // Set up exception state
    if let Some(heap_ref) = exception.as_object_ref() {
        let exception_info = ExceptionInfo::new(heap_ref, exception_type, throw_location);
        thread.exception_state_mut().set_exception(exception_info);
    }

    // Search for exception handler in current method
    if let Some(handler_match) = find_exception_handler(
        context,
        current_method,
        current_offset,
        Some(exception_type),
        thread.exception_state_mut(),
        None,
    )? {
        debug!(
            "  Found handler in current method 0x{:08X}",
            current_method.value()
        );
        thread.stack_mut().clear();
        let target_offset = apply_handler_match(
            &handler_match,
            exception,
            current_offset,
            current_method,
            thread,
        )?;
        interpreter.set_offset(target_offset);
        return Ok(true);
    }

    debug!(
        "  No handler in 0x{:08X}, unwinding...",
        current_method.value()
    );

    // No handler in current method — unwind call stack.
    //
    // Each frame stores: method (the method being executed) and return_offset
    // (the offset in the CALLER where execution resumes after this method returns).
    // When searching for exception handlers in the caller, we need the offset
    // within the caller where the call occurred — that's the return_offset from
    // the frame we're POPPING (the callee), not from the caller's own frame.
    let mut effective_exception_type = exception_type;
    loop {
        if let Some(pending) = thread.exception_state_mut().pop_finally() {
            interpreter.set_method(pending.method);
            interpreter.set_offset(pending.handler_offset);
            thread
                .exception_state_mut()
                .set_leave_target(pending.leave_target);
            return Ok(true);
        }

        // Capture the return_offset from the frame we're about to pop.
        // This is the call site offset in the CALLER method.
        // Also check if the frame was entered via reflection invoke — if so,
        // wrap the exception in TargetInvocationException as real .NET does.
        let (frame_return_offset, frame_is_reflection, frame_method) = thread
            .current_frame()
            .map_or((0, false, Token::new(0)), |f| {
                (f.return_offset(), f.is_reflection_invoke(), f.method())
            });

        // Build stack trace: record the frame being unwound
        if let Some(exc_info) = thread.exception_state_mut().exception_mut() {
            exc_info.push_stack_frame(InstructionLocation::new(frame_method, frame_return_offset));
        }
        if frame_is_reflection {
            let inner = thread
                .exception_state_mut()
                .take_exception_as_value()
                .unwrap_or_else(|| {
                    trace!("No pending exception for extraction in reflection frame");
                    exception.clone()
                });
            if let Ok((tie_val, tie_type)) =
                wrap_in_target_invocation_exception(address_space, &inner)
            {
                effective_exception_type = tie_type;
                if let Some(tie_ref) = tie_val.as_object_ref() {
                    let throw_loc = InstructionLocation::new(frame_method, frame_return_offset);
                    thread
                        .exception_state_mut()
                        .set_exception(ExceptionInfo::new(tie_ref, tie_type, throw_loc));
                }
            }
            debug!(
                "  Wrapping exception in TargetInvocationException (reflection invoke boundary)"
            );
        }

        thread.pop_frame();

        if thread.call_depth() == 0 {
            return Ok(false);
        }

        let caller_frame = thread
            .current_frame()
            .ok_or(EmulationError::InternalError {
                description: "call stack empty during exception unwinding".to_string(),
            })?;
        let caller_method = caller_frame.method();

        if let Some(handler_match) = find_exception_handler(
            context,
            caller_method,
            frame_return_offset,
            Some(effective_exception_type),
            thread.exception_state_mut(),
            None,
        )? {
            let exc = thread
                .exception_state_mut()
                .take_exception_as_value()
                .unwrap_or_else(|| {
                    trace!("No pending exception for extraction");
                    EmValue::Null
                });
            thread.stack_mut().clear();
            let target_offset = apply_handler_match(
                &handler_match,
                exc,
                frame_return_offset,
                caller_method,
                thread,
            )?;
            interpreter.set_method(caller_method);
            interpreter.set_offset(target_offset);
            return Ok(true);
        }
    }
}

/// Finds an exception handler for the given exception at the current offset.
///
/// Searches the method's exception handling table for a handler that covers
/// the current instruction offset and matches the exception type. Handlers
/// are searched in order (innermost first) per ECMA-335.
///
/// # Arguments
///
/// * `context` - The emulation context (for method metadata)
/// * `method_token` - Token of the method to search for handlers
/// * `current_offset` - Current instruction offset within the method
/// * `exception_type` - The resolved type token of the exception (if known)
/// * `exception_state` - Mutable exception state for recording active handlers
/// * `skip_handler_offset` - If provided, skip handlers at or before this offset (for rethrow)
///
/// # Returns
///
/// - `Ok(Some(HandlerMatch))` — A matching handler was found.
/// - `Ok(None)` — No handler matches (exception should propagate).
///
/// # Errors
///
/// Returns an error if the method cannot be resolved from metadata.
#[allow(clippy::too_many_arguments)]
pub fn find_exception_handler(
    context: &EmulationContext,
    method_token: Token,
    current_offset: u32,
    exception_type: Option<Token>,
    exception_state: &mut ThreadExceptionState,
    skip_handler_offset: Option<u32>,
) -> Result<Option<HandlerMatch>> {
    // Get the method's exception handlers — check synthetic methods first
    let clauses = if let Some(handlers) = context.get_synthetic_exception_handlers(method_token) {
        ExceptionClause::from_metadata_handlers(&handlers)
    } else {
        let method = context.get_method(method_token)?;
        let Some(body) = method.body.get() else {
            return Ok(None);
        };
        ExceptionClause::from_metadata_handlers(&body.exception_handlers)
    };

    // Cleanup clauses encountered while scanning, held locally until the outcome is known.
    //
    // Committing these to `exception_state` as they are found is wrong: a cleanup clause that
    // the exception never unwinds past must not run, and nothing would ever drain it, so it
    // would accumulate across every search. But dropping them all when a handler matches is
    // equally wrong — that was the defect this shape replaced. For
    // `try { try { throw } finally { F } } catch { C }` both clauses cover the throw offset and
    // the inner Finally comes first in the clause table, so the exception *does* unwind out of
    // the inner try to reach `C`, and `F` has to run on the way.
    //
    // The rule is containment, not scan order: on a match, commit exactly those candidates
    // whose try region is nested inside the matched clause's try region. The full clause is
    // kept rather than just its handler offset so that test can be made.
    let mut cleanup_candidates: Vec<&ExceptionClause> = Vec::new();

    // Search for a handler that covers the current offset
    // Handlers are processed in order - innermost handlers first (as per ECMA-335)
    for clause in &clauses {
        // Check if this clause's try region covers the current offset
        if !clause.is_in_try(current_offset) {
            continue;
        }

        // For rethrow, skip handlers at or before the current handler
        if let Some(skip_offset) = skip_handler_offset {
            if clause.handler_offset() <= skip_offset {
                continue;
            }
        }

        // Handle based on clause type
        match clause {
            ExceptionClause::Catch { catch_type, .. } => {
                // Check type compatibility
                let is_compatible = match exception_type {
                    Some(exc_token) => context.is_type_compatible(exc_token, *catch_type),
                    // Exception type unknown - accept (best effort)
                    None => true,
                };

                if is_compatible {
                    commit_nested_cleanups(
                        exception_state,
                        method_token,
                        clause,
                        &cleanup_candidates,
                    );
                    return Ok(Some(HandlerMatch::Catch {
                        method: method_token,
                        handler_offset: clause.handler_offset(),
                    }));
                }
                // Type mismatch - continue searching
            }

            ExceptionClause::Filter { filter_offset, .. } => {
                // Filter handler - need to execute filter code first
                // Store the handler offset so EndFilter knows where to jump
                let handler_offset = clause.handler_offset();
                commit_nested_cleanups(exception_state, method_token, clause, &cleanup_candidates);
                exception_state.enter_filter(handler_offset);
                return Ok(Some(HandlerMatch::Filter {
                    method: method_token,
                    filter_offset: *filter_offset,
                    handler_offset,
                }));
            }

            ExceptionClause::Finally { .. } | ExceptionClause::Fault { .. } => {
                // Runs during unwinding — whether it is reached depends on where the search
                // ends up, so it is only a candidate until then.
                cleanup_candidates.push(clause);
            }
        }
    }

    // No handler matched, so the exception unwinds out of this frame entirely and every
    // cleanup clause collected above really does run.
    for clause in cleanup_candidates {
        exception_state.push_finally(method_token, clause.handler_offset(), None);
    }

    Ok(None)
}

/// Queues the cleanup clauses the exception unwinds past on its way to `matched`.
///
/// A `finally`/`fault` whose try region is nested inside the matched handler's try region sits
/// between the throw site and that handler, so it must run before the handler is entered — the
/// `try { try { throw } finally { F } } catch { C }` case. One that merely *encloses* the
/// matched handler is not unwound past at all: the exception is caught inside it, and queueing
/// it would run cleanup for a region still being executed and leave an entry nothing drains.
fn commit_nested_cleanups(
    exception_state: &mut ThreadExceptionState,
    method_token: Token,
    matched: &ExceptionClause,
    candidates: &[&ExceptionClause],
) {
    let (outer_start, outer_end) = (matched.try_offset(), matched.try_end());

    for clause in candidates {
        let nested = clause.try_offset() >= outer_start && clause.try_end() <= outer_end
            // An identical region is not nested — `try {} catch {} finally {}` compiles to two
            // clauses over the same try, and per ECMA-335 the finally runs *after* the catch
            // completes, not before it is entered. Leave it to the normal exit path.
            && (clause.try_offset() != outer_start || clause.try_end() != outer_end);

        if nested {
            exception_state.push_finally(method_token, clause.handler_offset(), None);
        }
    }
}

/// Schedules finally blocks to execute when leaving a protected region.
///
/// Finds all `finally` handlers between the current offset and the leave
/// target, and schedules them for execution via the exception state's
/// pending-finally queue. Finally blocks are executed in innermost-first
/// order per ECMA-335.
///
/// # Arguments
///
/// * `context` — Assembly metadata context for method lookup.
/// * `method_token` — Token of the method containing the `leave` instruction.
/// * `current_offset` — Offset of the `leave` instruction.
/// * `leave_target` — Target offset the `leave` instruction wants to jump to.
/// * `exception_state` — Exception state where finally blocks are queued.
///
/// # Errors
///
/// Returns an error if the method's exception handling table cannot be read.
pub fn schedule_finally_blocks(
    context: &EmulationContext,
    method_token: Token,
    current_offset: u32,
    leave_target: u32,
    exception_state: &mut ThreadExceptionState,
) -> Result<()> {
    // Get the method's exception handlers
    let method = context.get_method(method_token)?;
    let Some(body) = method.body.get() else {
        return Ok(());
    };

    // Convert metadata handlers to exception clauses
    let clauses = ExceptionClause::from_metadata_handlers(&body.exception_handlers);

    // Find finally handlers that we're leaving
    // We need to execute finally blocks for any try region we're exiting
    let mut finally_blocks: Vec<(u32, u32)> = Vec::new();

    for clause in &clauses {
        if !clause.is_finally() {
            continue;
        }

        // Check if we're leaving this try region
        let inside_now = clause.is_in_try(current_offset);
        let inside_target = clause.is_in_try(leave_target);

        if inside_now && !inside_target {
            // We're exiting this try region - need to run finally
            finally_blocks.push((clause.handler_offset(), clause.try_offset()));
        }
    }

    // ECMA-335 §12.4.2.5 runs finally handlers innermost-first when a `leave` exits nested try
    // regions. `pending_finally` is a LIFO stack, so pop order is the reverse of push order:
    // to pop innermost-first we must push **outermost-first**.
    //
    // For nested regions the inner try starts at or after the outer one, so ascending
    // `try_offset` is outermost-first.
    finally_blocks.sort_by_key(|f| f.1);

    // The leave target belongs on the entry popped **last** — the outermost, i.e. the first
    // pushed. Only once every finally in the chain has run does control transfer to the leave
    // target; attaching it to any earlier entry means `handle_end_finally` consumes it and then
    // overwrites it with a later entry's `None`, leaving `endfinally` with neither a target nor
    // an exception and nothing to advance the instruction pointer.
    for (i, (handler_offset, _)) in finally_blocks.iter().enumerate() {
        let target = if i == 0 { Some(leave_target) } else { None };
        exception_state.push_finally(method_token, *handler_offset, target);
    }

    Ok(())
}

/// Resolves the type token from an exception value.
///
/// Determines the type of an exception by inspecting the value kind and
/// looking up heap objects when necessary.
///
/// # Arguments
///
/// * `exception` — The thrown exception value.
/// * `thread` — The emulation thread (for heap access on `ObjectRef` values).
///
/// # Returns
///
/// - `Some(Token)` for `ObjectRef` (from heap), `ValueType`, or `TypedRef`.
/// - `None` for primitive types or unresolvable values.
pub fn resolve_exception_type(exception: &EmValue, thread: &EmulationThread) -> Option<Token> {
    match exception {
        EmValue::ObjectRef(href) => thread.heap().get_type_token(*href).ok(),
        EmValue::ValueType { type_token, .. } | EmValue::TypedRef { type_token, .. } => {
            Some(*type_token)
        }
        _ => None,
    }
}

/// Applies a handler match result, pushing exception if needed and returning
/// the target offset.
///
/// For catch/filter handlers, pushes the exception onto the evaluation stack
/// so the handler code can access it. For catch handlers, also enters the
/// catch handler state for proper `rethrow` support.
///
/// # Arguments
///
/// * `handler_match` — The matched handler (catch, filter, finally, or fault).
/// * `exception` — The exception value to push for catch/filter handlers.
/// * `origin_offset` — Offset where the exception originated (for catch state).
/// * `method` — Token of the method containing the handler.
/// * `thread` — The emulation thread (for stack push and exception state).
///
/// # Returns
///
/// The instruction offset to jump to for handler execution.
///
/// # Errors
///
/// Returns an error if pushing the exception onto the evaluation stack fails.
pub fn apply_handler_match(
    handler_match: &HandlerMatch,
    exception: EmValue,
    origin_offset: u32,
    method: Token,
    thread: &mut EmulationThread,
) -> Result<u32> {
    match handler_match {
        HandlerMatch::Catch { handler_offset, .. } => {
            thread.push(exception)?;
            thread.exception_state_mut().enter_catch_handler(
                method,
                origin_offset,
                *handler_offset,
            );
            Ok(*handler_offset)
        }
        HandlerMatch::Filter { filter_offset, .. } => {
            thread.push(exception)?;
            Ok(*filter_offset)
        }
        HandlerMatch::Finally { handler_offset, .. }
        | HandlerMatch::Fault { handler_offset, .. } => Ok(*handler_offset),
    }
}

/// Wraps an exception in a `TypeInitializationException`.
///
/// Per ECMA-335, when a .cctor throws, the exception is wrapped in a
/// `TypeInitializationException` with the failing type's name as the message
/// and the original exception as `InnerException`.
///
/// # Arguments
///
/// * `address_space` — The shared address space for heap allocation.
/// * `inner_exception` — The original exception thrown by the .cctor.
/// * `type_name` — The name of the type whose .cctor failed.
///
/// # Returns
///
/// A tuple `(EmValue::ObjectRef, Token)` containing the TIE object and its type token.
///
/// # Errors
///
/// Returns an error if heap allocation fails.
pub fn wrap_in_type_initialization_exception(
    address_space: &AddressSpace,
    inner_exception: &EmValue,
    type_name: &str,
) -> Result<(EmValue, Token)> {
    let tie_type = synthetic_exception::TYPE_INITIALIZATION;
    let tie_ref = address_space.alloc_object(tie_type)?;

    // Set Message field to the failing type's name
    address_space.set_field(
        tie_ref,
        tokens::exception_fields::MESSAGE,
        EmValue::ObjectRef(address_space.heap().alloc_string(type_name)?),
    )?;

    // Set InnerException field to the original exception
    address_space.set_field(
        tie_ref,
        tokens::exception_fields::INNER_EXCEPTION,
        inner_exception.clone(),
    )?;

    Ok((EmValue::ObjectRef(tie_ref), tie_type))
}

/// Records a `.cctor` failure if the current frame is a static constructor.
///
/// When an exception escapes a `.cctor`, the CLR marks the type as
/// permanently failed. Subsequent accesses to that type re-throw the
/// stored exception. This function checks whether the current frame is a
/// `.cctor` and, if so, stores the exception in the [`CctorTracker`].
///
/// # Arguments
///
/// * `cctor_tracker` — Tracker for recording type initialization failures.
/// * `thread` — The emulation thread (for inspecting the current frame).
/// * `exception` — The exception value escaping the `.cctor`.
/// * `context` — Assembly metadata context for resolving the declaring type.
///
/// # Errors
///
/// Returns an error if heap allocation or cctor tracker operations fail.
pub fn track_cctor_failure_if_needed(
    cctor_tracker: &CctorTracker,
    thread: &EmulationThread,
    exception: &EmValue,
    context: &EmulationContext,
) -> Result<()> {
    let Some(frame) = thread.current_frame() else {
        return Ok(());
    };
    if !frame.is_cctor() {
        return Ok(());
    }

    // Find the type that owns this .cctor
    let cctor_token = frame.method();
    let Some(type_token) = context
        .assembly()
        .resolver()
        .declaring_type(cctor_token)
        .map(|t| t.token)
    else {
        return Ok(());
    };

    // Store the exception — use the heap ref if it's an ObjectRef,
    // otherwise allocate a string object describing the error
    let exception_ref = match exception {
        EmValue::ObjectRef(href) => *href,
        other => {
            let desc = format!("TypeInitializationException: {other}");
            thread.heap().alloc_string(&desc)?
        }
    };

    cctor_tracker.mark_type_failed(type_token, exception_ref)?;
    Ok(())
}

#[cfg(test)]
mod tests {
    use super::*;

    fn finally_clause(try_offset: u32, try_length: u32, handler_offset: u32) -> ExceptionClause {
        ExceptionClause::Finally {
            try_offset,
            try_length,
            handler_offset,
            handler_length: 4,
        }
    }

    fn catch_clause(try_offset: u32, try_length: u32, handler_offset: u32) -> ExceptionClause {
        ExceptionClause::Catch {
            try_offset,
            try_length,
            handler_offset,
            handler_length: 4,
            catch_type: Token::new(0x0100_0001),
        }
    }

    /// `try { try { throw } finally { F } } catch { C }` must run `F`.
    ///
    /// Both clauses cover the throw offset, and the inner Finally precedes the outer Catch in
    /// the clause table. The exception unwinds out of the inner try to reach `C`, so `F` sits
    /// on that path. Dropping every candidate on a match — which is what committing them only
    /// on the no-handler path amounts to — silently skipped it.
    #[test]
    fn a_finally_nested_inside_the_matched_try_is_queued() {
        let mut state = ThreadExceptionState::new();
        let inner_finally = finally_clause(10, 10, 30);
        let outer_catch = catch_clause(0, 40, 50);

        commit_nested_cleanups(
            &mut state,
            Token::new(0x0600_0001),
            &outer_catch,
            &[&inner_finally],
        );

        assert!(state.has_pending_finally(), "the inner finally must run");
        assert_eq!(state.pop_finally().map(|p| p.handler_offset), Some(30));
    }

    /// A `finally` that *encloses* the matched handler is not unwound past.
    ///
    /// The exception is caught inside that region, so its cleanup has not been reached.
    /// Queueing it would run cleanup for a region still executing and leave an entry that
    /// nothing drains — the accumulation the local-candidate design exists to prevent.
    #[test]
    fn a_finally_enclosing_the_matched_try_is_not_queued() {
        let mut state = ThreadExceptionState::new();
        let outer_finally = finally_clause(0, 100, 80);
        let inner_catch = catch_clause(10, 10, 30);

        commit_nested_cleanups(
            &mut state,
            Token::new(0x0600_0001),
            &inner_catch,
            &[&outer_finally],
        );

        assert!(!state.has_pending_finally());
    }

    /// `try { } catch { } finally { }` shares one try region across both clauses.
    ///
    /// ECMA-335 runs the finally *after* the catch completes, via the normal exit path — not
    /// before the catch is entered. An identical region is therefore not "nested".
    #[test]
    fn a_finally_over_the_same_try_region_is_not_queued() {
        let mut state = ThreadExceptionState::new();
        let finally = finally_clause(0, 20, 40);
        let catch = catch_clause(0, 20, 20);

        commit_nested_cleanups(&mut state, Token::new(0x0600_0001), &catch, &[&finally]);

        assert!(!state.has_pending_finally());
    }
}