Skip to main content

analyssa/
events.rs

1//! Event logging for the SSA optimization pipeline.
2//!
3//! Passes record events through an [`EventListener<T>`]; hosts that don't care
4//! pass [`NullListener`] to opt out without changing the call site. Hosts that
5//! do care use [`EventLog<T>`], a thread-safe append-only collection that
6//! itself impls `EventListener<T>`.
7//!
8//! # Architecture
9//!
10//! - [`EventKind`] — non-generic enum of event categories.
11//! - [`Event<T>`] — a recorded event, parameterized over the host's
12//!   `T::MethodRef` so each event can name the method it occurred in.
13//! - [`EventListener<T>`] — sink trait; `push` accepts a fully-formed event.
14//!   The default `record` method returns an [`EventBuilder`] for the fluent
15//!   `events.record(kind).at(...).message(...)` API.
16//! - [`NullListener`] — discards every event. Useful when running passes
17//!   without observation (unit tests, CI, callers that don't need an event
18//!   trace).
19//! - [`EventLog<T>`] — concrete listener storing events for later inspection,
20//!   summary, and filtering.
21//!
22//! # Example (analyssa-side, MockTarget)
23//!
24//! ```rust
25//! use analyssa::{events::{EventKind, EventLog, EventListener}, MockTarget};
26//!
27//! let log: EventLog<MockTarget> = EventLog::new();
28//! let method: u32 = 0x06000001;
29//!
30//! log.record(EventKind::ConstantFolded)
31//!     .at(method, 0x42)
32//!     .message("42 + 0 = 42");
33//!
34//! assert!(log.has(EventKind::ConstantFolded));
35//! ```
36
37use std::{
38    collections::{HashMap, HashSet},
39    fmt,
40    time::Duration,
41};
42
43use crate::target::Target;
44
45/// Categories of events that can be logged. Target-agnostic — labels are
46/// purely descriptive and carry no host types.
47#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
48pub enum EventKind {
49    /// A string was decrypted and inlined.
50    StringDecrypted,
51    /// A constant value was decrypted via emulation of a decryptor method.
52    ConstantDecrypted,
53    /// A constant value was folded/propagated.
54    ConstantFolded,
55    /// A conditional branch was simplified to unconditional.
56    BranchSimplified,
57    /// An instruction was removed.
58    InstructionRemoved,
59    /// A basic block was removed.
60    BlockRemoved,
61    /// A method call was inlined.
62    MethodInlined,
63    /// A phi node was simplified.
64    PhiSimplified,
65    /// An unknown value was resolved to a constant.
66    ValueResolved,
67    /// A method was marked as dead (unreachable).
68    MethodMarkedDead,
69    /// Control flow was restructured (e.g., unflattening).
70    ControlFlowRestructured,
71    /// An opaque predicate was identified and removed.
72    OpaquePredicateRemoved,
73    /// A copy operation was propagated away.
74    CopyPropagated,
75    /// An array was decrypted.
76    ArrayDecrypted,
77    /// An expensive operation was strength-reduced.
78    StrengthReduced,
79    /// Orphaned variables were removed from the variable table.
80    VariablesCompacted,
81    /// An encrypted method body was decrypted (anti-tamper).
82    MethodBodyDecrypted,
83    /// An encrypted resource was decrypted and re-injected (e.g. .NET Reactor
84    /// Stage 7 resource encryption).
85    ResourceDecrypted,
86    /// Anti-tamper protection was removed.
87    AntiTamperRemoved,
88    /// An obfuscation artifact was removed (method, type, metadata).
89    ArtifactRemoved,
90    /// Code regeneration completed.
91    CodeRegenerated,
92    /// A load was replaced by a value already available in memory — forwarded
93    /// from a dominating store, or from an earlier redundant load.
94    LoadForwarded,
95    /// A store was removed because a later store to the same location
96    /// overwrote it before any intervening read.
97    DeadStoreRemoved,
98}
99
100impl EventKind {
101    /// Returns a human-readable description of this event kind.
102    #[must_use]
103    pub fn description(&self) -> &'static str {
104        match self {
105            Self::LoadForwarded => "load forwarded",
106            Self::DeadStoreRemoved => "dead store removed",
107            Self::StringDecrypted => "string decrypted",
108            Self::ConstantDecrypted => "constant decrypted",
109            Self::ConstantFolded => "constant folded",
110            Self::BranchSimplified => "branch simplified",
111            Self::InstructionRemoved => "instruction removed",
112            Self::BlockRemoved => "block removed",
113            Self::MethodInlined => "method inlined",
114            Self::PhiSimplified => "phi simplified",
115            Self::ValueResolved => "value resolved",
116            Self::MethodMarkedDead => "method marked dead",
117            Self::ControlFlowRestructured => "control flow restructured",
118            Self::OpaquePredicateRemoved => "opaque predicate removed",
119            Self::CopyPropagated => "copy propagated",
120            Self::ArrayDecrypted => "array decrypted",
121            Self::StrengthReduced => "strength reduced",
122            Self::VariablesCompacted => "variables compacted",
123            Self::MethodBodyDecrypted => "method body decrypted",
124            Self::ResourceDecrypted => "resource decrypted",
125            Self::AntiTamperRemoved => "anti-tamper removed",
126            Self::ArtifactRemoved => "artifact removed",
127            Self::CodeRegenerated => "code regenerated",
128        }
129    }
130
131    /// Returns true if this event represents a code transformation.
132    #[must_use]
133    pub fn is_transformation(&self) -> bool {
134        !matches!(self, Self::CodeRegenerated)
135    }
136}
137
138impl fmt::Display for EventKind {
139    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
140        f.write_str(self.description())
141    }
142}
143
144/// A single logged event.
145#[derive(Debug, Clone)]
146pub struct Event<T: Target> {
147    /// The type of event.
148    pub kind: EventKind,
149    /// The method where the event occurred (if applicable).
150    pub method: Option<T::MethodRef>,
151    /// Location within the method (offset or block ID).
152    pub location: Option<usize>,
153    /// Human-readable description.
154    pub message: String,
155    /// Associated pass name (if from a pass).
156    pub pass: Option<String>,
157}
158
159impl<T: Target> fmt::Display for Event<T> {
160    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
161        write!(f, "[{}] {}", self.kind, self.message)
162    }
163}
164
165/// Sink for events recorded by passes. The `push` method accepts a
166/// fully-formed event; the default `record` method opens a fluent
167/// [`EventBuilder`] that calls `push` on drop.
168///
169/// `Self: Sized` is required for `record` so trait objects (`&dyn
170/// EventListener<T>`) can still receive events via `push` — they just can't
171/// open a builder. Concrete listeners use the builder.
172pub trait EventListener<T: Target> {
173    /// Append an event to this listener.
174    fn push(&self, event: Event<T>);
175
176    /// Returns `true` if this listener actually records events.
177    ///
178    /// Defaults to `true`. Sinks that discard everything (e.g.
179    /// [`NullListener`]) override this to `false` so the [`EventBuilder`] can
180    /// skip constructing the event and its default message entirely — avoiding
181    /// per-event allocation on the hot logging path when no one is observing.
182    fn is_enabled(&self) -> bool {
183        true
184    }
185
186    /// Number of events recorded so far.
187    ///
188    /// Used as a snapshot marker so a caller can summarise what happened between
189    /// two points. Defaults to `0` for sinks that keep no history, which makes
190    /// [`Self::count_by_kind_since`] correctly report nothing.
191    fn recorded_count(&self) -> usize {
192        0
193    }
194
195    /// Counts events by kind recorded at or after `offset`.
196    ///
197    /// Defaults to empty for sinks that keep no history. Callers should treat an
198    /// empty result as "no detail available" rather than "nothing happened".
199    fn count_by_kind_since(&self, offset: usize) -> HashMap<EventKind, usize> {
200        let _ = offset;
201        HashMap::new()
202    }
203
204    /// Open a fluent builder for an event of `kind`. The event is appended
205    /// when the builder is dropped, mirroring the legacy `EventLog::record`
206    /// API.
207    fn record(&self, kind: EventKind) -> EventBuilder<'_, T, Self>
208    where
209        Self: Sized,
210    {
211        EventBuilder::new(self, kind)
212    }
213}
214
215/// No-op listener: every event is silently discarded.
216///
217/// Used when callers want to run a pass without observation — e.g. unit
218/// tests that only check the resulting SSA, or production hosts that don't
219/// surface event traces.
220#[derive(Debug, Default, Clone, Copy)]
221pub struct NullListener;
222
223impl<T: Target> EventListener<T> for NullListener {
224    fn push(&self, _event: Event<T>) {}
225
226    fn is_enabled(&self) -> bool {
227        false
228    }
229}
230
231/// Builder for creating events with a fluent API. Created via
232/// [`EventListener::record`]. The event is automatically appended to the
233/// owning listener when the builder is dropped.
234pub struct EventBuilder<'a, T: Target, L: EventListener<T> + ?Sized> {
235    listener: &'a L,
236    kind: EventKind,
237    method: Option<T::MethodRef>,
238    location: Option<usize>,
239    message: Option<String>,
240    pass: Option<String>,
241}
242
243impl<'a, T: Target, L: EventListener<T> + ?Sized> EventBuilder<'a, T, L> {
244    fn new(listener: &'a L, kind: EventKind) -> Self {
245        Self {
246            listener,
247            kind,
248            method: None,
249            location: None,
250            message: None,
251            pass: None,
252        }
253    }
254
255    /// Sets the method and location where the event occurred. Accepts
256    /// anything convertible into `T::MethodRef` so hosts whose metadata uses
257    /// a richer wrapper type can pass the underlying ID directly.
258    pub fn at(mut self, method: impl Into<T::MethodRef>, location: usize) -> Self {
259        self.method = Some(method.into());
260        self.location = Some(location);
261        self
262    }
263
264    /// Sets only the method (for method-level events without specific location).
265    pub fn method(mut self, method: impl Into<T::MethodRef>) -> Self {
266        self.method = Some(method.into());
267        self
268    }
269
270    /// Sets the location (for when method is already set or not applicable).
271    pub fn location(mut self, location: usize) -> Self {
272        self.location = Some(location);
273        self
274    }
275
276    /// Sets a custom message describing the event.
277    pub fn message(mut self, msg: impl Into<String>) -> Self {
278        self.message = Some(msg.into());
279        self
280    }
281
282    /// Associates this event with a specific pass.
283    pub fn pass(mut self, pass_name: impl Into<String>) -> Self {
284        self.pass = Some(pass_name.into());
285        self
286    }
287}
288
289impl<T: Target, L: EventListener<T> + ?Sized> Drop for EventBuilder<'_, T, L> {
290    fn drop(&mut self) {
291        // Skip building the event (and its default-message allocation) entirely
292        // when the sink discards everything.
293        if !self.listener.is_enabled() {
294            return;
295        }
296
297        let message = self
298            .message
299            .take()
300            .unwrap_or_else(|| self.kind.description().to_string());
301
302        let event = Event {
303            kind: self.kind,
304            method: self.method.take(),
305            location: self.location.take(),
306            message,
307            pass: self.pass.take(),
308        };
309
310        self.listener.push(event);
311    }
312}
313
314/// Concrete event sink storing events for later inspection, summary, and
315/// filtering. Thread-safe append: events can be recorded concurrently from
316/// multiple threads using shared references (`&self`) thanks to
317/// [`boxcar::Vec`].
318pub struct EventLog<T: Target> {
319    events: boxcar::Vec<Event<T>>,
320}
321
322impl<T: Target> Default for EventLog<T> {
323    fn default() -> Self {
324        Self {
325            events: boxcar::Vec::new(),
326        }
327    }
328}
329
330impl<T: Target> fmt::Debug for EventLog<T> {
331    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
332        f.debug_struct("EventLog")
333            .field("len", &self.len())
334            .finish()
335    }
336}
337
338impl<T: Target> Clone for EventLog<T> {
339    fn clone(&self) -> Self {
340        let new_log = Self::new();
341        for (_, event) in &self.events {
342            new_log.events.push(event.clone());
343        }
344        new_log
345    }
346}
347
348impl<T: Target> EventListener<T> for EventLog<T> {
349    fn push(&self, event: Event<T>) {
350        self.events.push(event);
351    }
352
353    fn recorded_count(&self) -> usize {
354        self.len()
355    }
356
357    fn count_by_kind_since(&self, offset: usize) -> HashMap<EventKind, usize> {
358        Self::count_by_kind_since(self, offset)
359    }
360}
361
362impl<T: Target> EventLog<T> {
363    /// Creates an empty event log.
364    #[must_use]
365    pub fn new() -> Self {
366        Self::default()
367    }
368
369    /// Open a fluent builder for an event of `kind`. Mirrors
370    /// [`EventListener::record`] as an inherent method so callers don't have
371    /// to import the trait. The event is appended when the builder is
372    /// dropped.
373    pub fn record(&self, kind: EventKind) -> EventBuilder<'_, T, Self> {
374        EventBuilder::new(self, kind)
375    }
376
377    /// Returns true if no events have been logged.
378    #[must_use]
379    pub fn is_empty(&self) -> bool {
380        self.events.count() == 0
381    }
382
383    /// Returns the total number of events.
384    #[must_use]
385    pub fn len(&self) -> usize {
386        self.events.count()
387    }
388
389    /// Merges another event log into this one.
390    pub fn merge(&self, other: &EventLog<T>) {
391        for (_, event) in &other.events {
392            self.events.push(event.clone());
393        }
394    }
395
396    /// Returns true if any event of the given kind exists.
397    #[must_use]
398    pub fn has(&self, kind: EventKind) -> bool {
399        self.events.iter().any(|(_, e)| e.kind == kind)
400    }
401
402    /// Returns true if any of the given event kinds exist.
403    #[must_use]
404    pub fn has_any(&self, kinds: &[EventKind]) -> bool {
405        self.events.iter().any(|(_, e)| kinds.contains(&e.kind))
406    }
407
408    /// Counts events of the given kind.
409    #[must_use]
410    pub fn count_kind(&self, kind: EventKind) -> usize {
411        self.events.iter().filter(|(_, e)| e.kind == kind).count()
412    }
413
414    /// Returns an iterator over all events.
415    pub fn iter(&self) -> impl Iterator<Item = &Event<T>> {
416        self.events.iter().map(|(_, e)| e)
417    }
418
419    /// Returns an iterator over events of a specific kind.
420    pub fn filter_kind(&self, kind: EventKind) -> impl Iterator<Item = &Event<T>> + '_ {
421        self.events
422            .iter()
423            .filter_map(move |(_, e)| if e.kind == kind { Some(e) } else { None })
424    }
425
426    /// Takes ownership of the events by cloning into a new EventLog.
427    ///
428    /// This is useful when the context is being consumed and you need to
429    /// extract the events. Since `boxcar::Vec` is append-only and doesn't
430    /// support draining, this creates a clone.
431    #[must_use]
432    pub fn take(&self) -> EventLog<T> {
433        self.clone()
434    }
435
436    /// Consumes the log and returns its events, moving them out instead of
437    /// cloning.
438    ///
439    /// Prefer this over [`take`](Self::take) when the log is owned and no
440    /// longer needed: `take` must deep-clone every event (including its
441    /// `String` message) because `boxcar::Vec` is append-only, whereas this
442    /// drains the backing store by value.
443    #[must_use]
444    pub fn into_events(self) -> Vec<Event<T>> {
445        self.events.into_iter().collect()
446    }
447
448    /// Returns an iterator over events for a specific method.
449    pub fn filter_method<'a>(
450        &'a self,
451        method: &'a T::MethodRef,
452    ) -> impl Iterator<Item = &'a Event<T>> + 'a {
453        self.events.iter().filter_map(move |(_, e)| {
454            if e.method.as_ref() == Some(method) {
455                Some(e)
456            } else {
457                None
458            }
459        })
460    }
461
462    /// Returns an iterator over transformation events only.
463    pub fn transformations(&self) -> impl Iterator<Item = &Event<T>> + '_ {
464        self.events.iter().filter_map(|(_, e)| {
465            if e.kind.is_transformation() {
466                Some(e)
467            } else {
468                None
469            }
470        })
471    }
472
473    /// Counts events grouped by kind.
474    #[must_use]
475    pub fn count_by_kind(&self) -> HashMap<EventKind, usize> {
476        let mut counts: HashMap<EventKind, usize> = HashMap::new();
477        for (_, event) in &self.events {
478            let entry = counts.entry(event.kind).or_insert(0);
479            *entry = entry.saturating_add(1);
480        }
481        counts
482    }
483
484    /// Counts events grouped by kind, starting from the given offset.
485    ///
486    /// Used by the scheduler to compute per-pass event deltas without
487    /// iterating the entire log.
488    #[must_use]
489    pub fn count_by_kind_since(&self, offset: usize) -> HashMap<EventKind, usize> {
490        let mut counts: HashMap<EventKind, usize> = HashMap::new();
491        for (idx, event) in &self.events {
492            if idx >= offset {
493                let entry = counts.entry(event.kind).or_insert(0);
494                *entry = entry.saturating_add(1);
495            }
496        }
497        counts
498    }
499
500    /// Returns the number of transformation events.
501    #[must_use]
502    pub fn transformation_count(&self) -> usize {
503        self.events
504            .iter()
505            .filter(|(_, e)| e.kind.is_transformation())
506            .count()
507    }
508
509    /// Returns the number of unique methods with events.
510    #[must_use]
511    pub fn methods_affected(&self) -> usize {
512        self.events
513            .iter()
514            .filter_map(|(_, e)| e.method.as_ref())
515            .collect::<HashSet<_>>()
516            .len()
517    }
518
519    /// Generates a human-readable summary of all events.
520    #[must_use]
521    pub fn summary(&self) -> String {
522        if self.is_empty() {
523            return "no events".to_string();
524        }
525
526        let counts = self.count_by_kind();
527
528        let mut parts: Vec<String> = counts
529            .iter()
530            .filter(|(k, _)| k.is_transformation())
531            .map(|(kind, count)| format!("{} {}", count, kind.description()))
532            .collect();
533
534        if parts.is_empty() {
535            return format!("{} events", self.len());
536        }
537
538        parts.sort();
539        parts.join(", ")
540    }
541}
542
543/// Iterator wrapper for [`EventLog`] that yields `&Event<T>`.
544pub struct EventLogIter<'a, T: Target> {
545    inner: boxcar::Iter<'a, Event<T>>,
546}
547
548impl<'a, T: Target> Iterator for EventLogIter<'a, T> {
549    type Item = &'a Event<T>;
550
551    fn next(&mut self) -> Option<Self::Item> {
552        self.inner.next().map(|(_, e)| e)
553    }
554}
555
556impl<'a, T: Target> IntoIterator for &'a EventLog<T> {
557    type Item = &'a Event<T>;
558    type IntoIter = EventLogIter<'a, T>;
559
560    fn into_iter(self) -> Self::IntoIter {
561        EventLogIter {
562            inner: self.events.iter(),
563        }
564    }
565}
566
567impl<T: Target> Extend<Event<T>> for EventLog<T> {
568    fn extend<I: IntoIterator<Item = Event<T>>>(&mut self, iter: I) {
569        for event in iter {
570            self.events.push(event);
571        }
572    }
573}
574
575impl<T: Target> FromIterator<Event<T>> for EventLog<T> {
576    fn from_iter<I: IntoIterator<Item = Event<T>>>(iter: I) -> Self {
577        let log = Self::new();
578        for event in iter {
579            log.events.push(event);
580        }
581        log
582    }
583}
584
585/// Statistics derived from an [`EventLog`]. Counts are by [`EventKind`] and
586/// independent of `T`, so this struct is not generic.
587#[derive(Debug, Clone, Default)]
588pub struct DerivedStats {
589    /// Number of methods that had any transformations.
590    pub methods_transformed: usize,
591    /// Number of strings decrypted.
592    pub strings_decrypted: usize,
593    /// Number of arrays decrypted.
594    pub arrays_decrypted: usize,
595    /// Number of constants folded.
596    pub constants_folded: usize,
597    /// Number of constants decrypted.
598    pub constants_decrypted: usize,
599    /// Number of instructions removed.
600    pub instructions_removed: usize,
601    /// Number of blocks removed.
602    pub blocks_removed: usize,
603    /// Number of branches simplified.
604    pub branches_simplified: usize,
605    /// Number of opaque predicates removed.
606    pub opaque_predicates_removed: usize,
607    /// Number of methods inlined.
608    pub methods_inlined: usize,
609    /// Number of methods marked dead.
610    pub methods_marked_dead: usize,
611    /// Number of methods with code regenerated.
612    pub methods_regenerated: usize,
613    /// Number of artifacts removed (methods, types, metadata).
614    pub artifacts_removed: usize,
615    /// Number of loads replaced by an already-available value.
616    pub loads_forwarded: usize,
617    /// Number of dead stores removed.
618    pub dead_stores_removed: usize,
619    /// Number of pass iterations.
620    pub iterations: usize,
621    /// Processing time.
622    pub total_time: Duration,
623}
624
625impl DerivedStats {
626    /// Computes statistics from an event log.
627    #[must_use]
628    pub fn from_log<T: Target>(log: &EventLog<T>) -> Self {
629        let counts = log.count_by_kind();
630        let get = |kind: EventKind| counts.get(&kind).copied().unwrap_or(0);
631
632        Self {
633            methods_transformed: log.methods_affected(),
634            strings_decrypted: get(EventKind::StringDecrypted),
635            arrays_decrypted: get(EventKind::ArrayDecrypted),
636            constants_folded: get(EventKind::ConstantFolded),
637            constants_decrypted: get(EventKind::ConstantDecrypted),
638            instructions_removed: get(EventKind::InstructionRemoved),
639            blocks_removed: get(EventKind::BlockRemoved),
640            branches_simplified: get(EventKind::BranchSimplified),
641            opaque_predicates_removed: get(EventKind::OpaquePredicateRemoved),
642            methods_inlined: get(EventKind::MethodInlined),
643            methods_marked_dead: get(EventKind::MethodMarkedDead),
644            methods_regenerated: get(EventKind::CodeRegenerated),
645            artifacts_removed: get(EventKind::ArtifactRemoved),
646            loads_forwarded: get(EventKind::LoadForwarded),
647            dead_stores_removed: get(EventKind::DeadStoreRemoved),
648            iterations: 0,
649            total_time: Duration::ZERO,
650        }
651    }
652
653    /// Sets the total processing time.
654    #[must_use]
655    pub fn with_time(mut self, time: Duration) -> Self {
656        self.total_time = time;
657        self
658    }
659
660    /// Sets the number of iterations.
661    #[must_use]
662    pub fn with_iterations(mut self, iterations: usize) -> Self {
663        self.iterations = iterations;
664        self
665    }
666
667    /// Generates a human-readable summary.
668    #[must_use]
669    pub fn summary(&self) -> String {
670        let mut parts = Vec::new();
671
672        if self.methods_transformed > 0 {
673            parts.push(format!("{} methods", self.methods_transformed));
674        }
675
676        if self.strings_decrypted > 0 {
677            parts.push(format!("{} strings decrypted", self.strings_decrypted));
678        }
679        if self.arrays_decrypted > 0 {
680            parts.push(format!("{} arrays decrypted", self.arrays_decrypted));
681        }
682        if self.constants_decrypted > 0 {
683            parts.push(format!("{} constants decrypted", self.constants_decrypted));
684        }
685
686        if self.constants_folded > 0 {
687            parts.push(format!("{} constants folded", self.constants_folded));
688        }
689        if self.instructions_removed > 0 {
690            parts.push(format!(
691                "{} instructions removed",
692                self.instructions_removed
693            ));
694        }
695        if self.blocks_removed > 0 {
696            parts.push(format!("{} blocks removed", self.blocks_removed));
697        }
698        if self.branches_simplified > 0 {
699            parts.push(format!("{} branches simplified", self.branches_simplified));
700        }
701        if self.methods_inlined > 0 {
702            parts.push(format!("{} inlined", self.methods_inlined));
703        }
704        if self.opaque_predicates_removed > 0 {
705            parts.push(format!(
706                "{} opaque predicates",
707                self.opaque_predicates_removed
708            ));
709        }
710
711        if self.methods_marked_dead > 0 {
712            parts.push(format!("{} dead methods", self.methods_marked_dead));
713        }
714        if self.methods_regenerated > 0 {
715            parts.push(format!("{} regenerated", self.methods_regenerated));
716        }
717        if self.artifacts_removed > 0 {
718            parts.push(format!("{} artifacts removed", self.artifacts_removed));
719        }
720
721        let stats = if parts.is_empty() {
722            "no transformations".to_string()
723        } else {
724            parts.join(", ")
725        };
726
727        if self.total_time.as_millis() > 0 {
728            format!(
729                "{} in {:?} ({} iterations)",
730                stats, self.total_time, self.iterations
731            )
732        } else {
733            stats
734        }
735    }
736}
737
738impl fmt::Display for DerivedStats {
739    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
740        f.write_str(&self.summary())
741    }
742}
743
744/// Truncates a string for display, adding ellipsis if needed.
745#[must_use]
746pub fn truncate_string(s: &str, max_len: usize) -> String {
747    if s.len() <= max_len {
748        s.to_string()
749    } else {
750        let end = max_len.saturating_sub(3);
751        let split = s
752            .char_indices()
753            .map(|(i, _)| i)
754            .take_while(|&i| i <= end)
755            .last()
756            .unwrap_or(0);
757        format!("{}...", &s[..split])
758    }
759}
760
761#[cfg(test)]
762mod tests {
763    use std::{sync::Arc, thread};
764
765    use super::*;
766    use crate::testing::MockTarget;
767
768    type Method = <MockTarget as Target>::MethodRef;
769
770    fn method(id: u32) -> Method {
771        id
772    }
773
774    #[test]
775    fn empty_log() {
776        let log: EventLog<MockTarget> = EventLog::new();
777        assert!(log.is_empty());
778        assert_eq!(log.len(), 0);
779        assert!(!log.has(EventKind::StringDecrypted));
780    }
781
782    #[test]
783    fn record_event() {
784        let log: EventLog<MockTarget> = EventLog::new();
785        let m = method(0x06000001);
786
787        log.record(EventKind::StringDecrypted)
788            .at(m, 0x10)
789            .message("decrypted: \"hello\"");
790
791        assert!(!log.is_empty());
792        assert_eq!(log.len(), 1);
793        assert!(log.has(EventKind::StringDecrypted));
794
795        let event = log.iter().next().unwrap();
796        assert_eq!(event.method, Some(m));
797        assert_eq!(event.location, Some(0x10));
798        assert_eq!(event.message, "decrypted: \"hello\"");
799    }
800
801    #[test]
802    fn null_listener_discards() {
803        let listener = NullListener;
804        let m = method(0x06000001);
805
806        EventListener::<MockTarget>::record(&listener, EventKind::StringDecrypted)
807            .at(m, 0x10)
808            .message("dropped on the floor");
809
810        // No public observation surface — the test is that nothing panics
811        // and the listener compiles.
812    }
813
814    #[test]
815    fn multiple_events() {
816        let log: EventLog<MockTarget> = EventLog::new();
817        let m = method(0x06000001);
818
819        log.record(EventKind::StringDecrypted)
820            .at(m, 0x10)
821            .message("first");
822        log.record(EventKind::ConstantFolded)
823            .at(m, 0x20)
824            .message("second");
825
826        assert_eq!(log.len(), 2);
827        assert!(log.has(EventKind::StringDecrypted));
828        assert!(log.has(EventKind::ConstantFolded));
829        assert!(!log.has(EventKind::BlockRemoved));
830    }
831
832    #[test]
833    fn has_any() {
834        let log: EventLog<MockTarget> = EventLog::new();
835        log.record(EventKind::StringDecrypted)
836            .at(method(0x06000001), 0x10);
837
838        assert!(log.has_any(&[EventKind::StringDecrypted, EventKind::ArrayDecrypted]));
839        assert!(!log.has_any(&[EventKind::BlockRemoved, EventKind::MethodInlined]));
840    }
841
842    #[test]
843    fn merge() {
844        let log1: EventLog<MockTarget> = EventLog::new();
845        let log2: EventLog<MockTarget> = EventLog::new();
846        let m = method(0x06000001);
847
848        log1.record(EventKind::StringDecrypted).at(m, 0x10);
849        log2.record(EventKind::ConstantFolded).at(m, 0x20);
850
851        log1.merge(&log2);
852
853        assert_eq!(log1.len(), 2);
854        assert!(log1.has(EventKind::StringDecrypted));
855        assert!(log1.has(EventKind::ConstantFolded));
856    }
857
858    #[test]
859    fn summary() {
860        let log: EventLog<MockTarget> = EventLog::new();
861        let m = method(0x06000001);
862
863        log.record(EventKind::StringDecrypted).at(m, 0x10);
864        log.record(EventKind::StringDecrypted).at(m, 0x20);
865        log.record(EventKind::ConstantFolded).at(m, 0x30);
866
867        let summary = log.summary();
868        assert!(summary.contains("2 string decrypted"));
869        assert!(summary.contains("1 constant folded"));
870    }
871
872    #[test]
873    fn count_by_kind() {
874        let log: EventLog<MockTarget> = EventLog::new();
875        let m = method(0x06000001);
876
877        log.record(EventKind::StringDecrypted).at(m, 0x10);
878        log.record(EventKind::StringDecrypted).at(m, 0x20);
879        log.record(EventKind::ConstantFolded).at(m, 0x30);
880
881        let counts = log.count_by_kind();
882        assert_eq!(counts.get(&EventKind::StringDecrypted), Some(&2));
883        assert_eq!(counts.get(&EventKind::ConstantFolded), Some(&1));
884        assert_eq!(counts.get(&EventKind::BlockRemoved), None);
885    }
886
887    #[test]
888    fn count_by_kind_since() {
889        let log: EventLog<MockTarget> = EventLog::new();
890        let m = method(0x06000001);
891
892        log.record(EventKind::StringDecrypted).at(m, 0x10);
893        log.record(EventKind::StringDecrypted).at(m, 0x20);
894
895        let offset = log.len();
896
897        log.record(EventKind::ConstantFolded).at(m, 0x30);
898        log.record(EventKind::ConstantFolded).at(m, 0x40);
899        log.record(EventKind::StringDecrypted).at(m, 0x50);
900
901        let counts = log.count_by_kind_since(offset);
902        assert_eq!(counts.get(&EventKind::ConstantFolded), Some(&2));
903        assert_eq!(counts.get(&EventKind::StringDecrypted), Some(&1));
904        assert_eq!(counts.get(&EventKind::BlockRemoved), None);
905
906        let all = log.count_by_kind_since(0);
907        assert_eq!(all.get(&EventKind::StringDecrypted), Some(&3));
908        assert_eq!(all.get(&EventKind::ConstantFolded), Some(&2));
909    }
910
911    #[test]
912    fn derived_stats() {
913        let log: EventLog<MockTarget> = EventLog::new();
914        let m1 = method(0x06000001);
915        let m2 = method(0x06000002);
916
917        log.record(EventKind::StringDecrypted).at(m1, 0x10);
918        log.record(EventKind::StringDecrypted).at(m2, 0x20);
919        log.record(EventKind::ConstantFolded).at(m1, 0x30);
920
921        let stats = DerivedStats::from_log(&log);
922        assert_eq!(stats.methods_transformed, 2);
923        assert_eq!(stats.strings_decrypted, 2);
924        assert_eq!(stats.constants_folded, 1);
925    }
926
927    #[test]
928    fn filter_methods() {
929        let log: EventLog<MockTarget> = EventLog::new();
930        let m1 = method(0x06000001);
931        let m2 = method(0x06000002);
932
933        log.record(EventKind::StringDecrypted).at(m1, 0x10);
934        log.record(EventKind::ConstantFolded).at(m2, 0x20);
935        log.record(EventKind::BlockRemoved).at(m1, 0x30);
936
937        let m1_events: Vec<_> = log.filter_method(&m1).collect();
938        assert_eq!(m1_events.len(), 2);
939    }
940
941    #[test]
942    fn transformations_filter() {
943        let log: EventLog<MockTarget> = EventLog::new();
944        let m = method(0x06000001);
945
946        log.record(EventKind::StringDecrypted).at(m, 0x10);
947        log.record(EventKind::BlockRemoved).at(m, 0x20);
948
949        let transformations: Vec<_> = log.transformations().collect();
950        assert_eq!(transformations.len(), 2);
951    }
952
953    #[test]
954    fn event_with_pass() {
955        let log: EventLog<MockTarget> = EventLog::new();
956        let m = method(0x06000001);
957
958        log.record(EventKind::ConstantFolded)
959            .at(m, 0x10)
960            .pass("ConstantFolding")
961            .message("42 + 0 → 42");
962
963        let event = log.iter().next().unwrap();
964        assert_eq!(event.pass.as_deref(), Some("ConstantFolding"));
965    }
966
967    #[test]
968    fn default_message() {
969        let log: EventLog<MockTarget> = EventLog::new();
970        let m = method(0x06000001);
971
972        log.record(EventKind::StringDecrypted).at(m, 0x10);
973
974        let event = log.iter().next().unwrap();
975        assert_eq!(event.message, "string decrypted");
976    }
977
978    #[test]
979    fn thread_safe_append() {
980        let log: Arc<EventLog<MockTarget>> = Arc::new(EventLog::new());
981        let mut handles = vec![];
982
983        for i in 0..4u32 {
984            let log_clone = Arc::clone(&log);
985            handles.push(thread::spawn(move || {
986                for j in 0..100u32 {
987                    let m = method(
988                        0x06000000u32
989                            .saturating_add(i.saturating_mul(100))
990                            .saturating_add(j),
991                    );
992                    log_clone
993                        .record(EventKind::StringDecrypted)
994                        .at(m, j as usize)
995                        .message(format!("thread {i} event {j}"));
996                }
997            }));
998        }
999
1000        for handle in handles {
1001            handle.join().unwrap();
1002        }
1003
1004        assert_eq!(log.len(), 400);
1005    }
1006
1007    #[test]
1008    fn into_events_moves_without_cloning() {
1009        let log: EventLog<MockTarget> = EventLog::new();
1010        log.record(EventKind::StringDecrypted)
1011            .at(method(0x0600_0001), 0)
1012            .message("a");
1013        log.record(EventKind::ConstantFolded)
1014            .at(method(0x0600_0002), 1)
1015            .message("b");
1016        let events = log.into_events();
1017        assert_eq!(events.len(), 2);
1018        assert_eq!(events[0].kind, EventKind::StringDecrypted);
1019        assert_eq!(events[1].kind, EventKind::ConstantFolded);
1020    }
1021
1022    #[test]
1023    fn null_listener_is_disabled() {
1024        let null = NullListener;
1025        assert!(!EventListener::<MockTarget>::is_enabled(&null));
1026        // Recording through a disabled listener must not panic and yields nothing.
1027        EventListener::<MockTarget>::record(&null, EventKind::StringDecrypted)
1028            .at(method(0x0600_0003), 0)
1029            .message("ignored");
1030    }
1031
1032    #[test]
1033    fn truncate_string_short() {
1034        assert_eq!(truncate_string("hi", 10), "hi");
1035    }
1036
1037    #[test]
1038    fn truncate_string_long() {
1039        let result = truncate_string("hello world", 8);
1040        assert!(result.ends_with("..."));
1041        assert!(result.len() <= 8);
1042    }
1043}