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}
93
94impl EventKind {
95    /// Returns a human-readable description of this event kind.
96    #[must_use]
97    pub fn description(&self) -> &'static str {
98        match self {
99            Self::StringDecrypted => "string decrypted",
100            Self::ConstantDecrypted => "constant decrypted",
101            Self::ConstantFolded => "constant folded",
102            Self::BranchSimplified => "branch simplified",
103            Self::InstructionRemoved => "instruction removed",
104            Self::BlockRemoved => "block removed",
105            Self::MethodInlined => "method inlined",
106            Self::PhiSimplified => "phi simplified",
107            Self::ValueResolved => "value resolved",
108            Self::MethodMarkedDead => "method marked dead",
109            Self::ControlFlowRestructured => "control flow restructured",
110            Self::OpaquePredicateRemoved => "opaque predicate removed",
111            Self::CopyPropagated => "copy propagated",
112            Self::ArrayDecrypted => "array decrypted",
113            Self::StrengthReduced => "strength reduced",
114            Self::VariablesCompacted => "variables compacted",
115            Self::MethodBodyDecrypted => "method body decrypted",
116            Self::ResourceDecrypted => "resource decrypted",
117            Self::AntiTamperRemoved => "anti-tamper removed",
118            Self::ArtifactRemoved => "artifact removed",
119            Self::CodeRegenerated => "code regenerated",
120        }
121    }
122
123    /// Returns true if this event represents a code transformation.
124    #[must_use]
125    pub fn is_transformation(&self) -> bool {
126        !matches!(self, Self::CodeRegenerated)
127    }
128}
129
130impl fmt::Display for EventKind {
131    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
132        f.write_str(self.description())
133    }
134}
135
136/// A single logged event.
137#[derive(Debug, Clone)]
138pub struct Event<T: Target> {
139    /// The type of event.
140    pub kind: EventKind,
141    /// The method where the event occurred (if applicable).
142    pub method: Option<T::MethodRef>,
143    /// Location within the method (offset or block ID).
144    pub location: Option<usize>,
145    /// Human-readable description.
146    pub message: String,
147    /// Associated pass name (if from a pass).
148    pub pass: Option<String>,
149}
150
151impl<T: Target> fmt::Display for Event<T> {
152    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
153        write!(f, "[{}] {}", self.kind, self.message)
154    }
155}
156
157/// Sink for events recorded by passes. The `push` method accepts a
158/// fully-formed event; the default `record` method opens a fluent
159/// [`EventBuilder`] that calls `push` on drop.
160///
161/// `Self: Sized` is required for `record` so trait objects (`&dyn
162/// EventListener<T>`) can still receive events via `push` — they just can't
163/// open a builder. Concrete listeners use the builder.
164pub trait EventListener<T: Target> {
165    /// Append an event to this listener.
166    fn push(&self, event: Event<T>);
167
168    /// Open a fluent builder for an event of `kind`. The event is appended
169    /// when the builder is dropped, mirroring the legacy `EventLog::record`
170    /// API.
171    fn record(&self, kind: EventKind) -> EventBuilder<'_, T, Self>
172    where
173        Self: Sized,
174    {
175        EventBuilder::new(self, kind)
176    }
177}
178
179/// No-op listener: every event is silently discarded.
180///
181/// Used when callers want to run a pass without observation — e.g. unit
182/// tests that only check the resulting SSA, or production hosts that don't
183/// surface event traces.
184#[derive(Debug, Default, Clone, Copy)]
185pub struct NullListener;
186
187impl<T: Target> EventListener<T> for NullListener {
188    fn push(&self, _event: Event<T>) {}
189}
190
191/// Builder for creating events with a fluent API. Created via
192/// [`EventListener::record`]. The event is automatically appended to the
193/// owning listener when the builder is dropped.
194pub struct EventBuilder<'a, T: Target, L: EventListener<T> + ?Sized> {
195    listener: &'a L,
196    kind: EventKind,
197    method: Option<T::MethodRef>,
198    location: Option<usize>,
199    message: Option<String>,
200    pass: Option<String>,
201}
202
203impl<'a, T: Target, L: EventListener<T> + ?Sized> EventBuilder<'a, T, L> {
204    fn new(listener: &'a L, kind: EventKind) -> Self {
205        Self {
206            listener,
207            kind,
208            method: None,
209            location: None,
210            message: None,
211            pass: None,
212        }
213    }
214
215    /// Sets the method and location where the event occurred. Accepts
216    /// anything convertible into `T::MethodRef` so hosts whose metadata uses
217    /// a richer wrapper type can pass the underlying ID directly.
218    pub fn at(mut self, method: impl Into<T::MethodRef>, location: usize) -> Self {
219        self.method = Some(method.into());
220        self.location = Some(location);
221        self
222    }
223
224    /// Sets only the method (for method-level events without specific location).
225    pub fn method(mut self, method: impl Into<T::MethodRef>) -> Self {
226        self.method = Some(method.into());
227        self
228    }
229
230    /// Sets the location (for when method is already set or not applicable).
231    pub fn location(mut self, location: usize) -> Self {
232        self.location = Some(location);
233        self
234    }
235
236    /// Sets a custom message describing the event.
237    pub fn message(mut self, msg: impl Into<String>) -> Self {
238        self.message = Some(msg.into());
239        self
240    }
241
242    /// Associates this event with a specific pass.
243    pub fn pass(mut self, pass_name: impl Into<String>) -> Self {
244        self.pass = Some(pass_name.into());
245        self
246    }
247}
248
249impl<T: Target, L: EventListener<T> + ?Sized> Drop for EventBuilder<'_, T, L> {
250    fn drop(&mut self) {
251        let message = self
252            .message
253            .take()
254            .unwrap_or_else(|| self.kind.description().to_string());
255
256        let event = Event {
257            kind: self.kind,
258            method: self.method.take(),
259            location: self.location.take(),
260            message,
261            pass: self.pass.take(),
262        };
263
264        self.listener.push(event);
265    }
266}
267
268/// Concrete event sink storing events for later inspection, summary, and
269/// filtering. Thread-safe append: events can be recorded concurrently from
270/// multiple threads using shared references (`&self`) thanks to
271/// [`boxcar::Vec`].
272pub struct EventLog<T: Target> {
273    events: boxcar::Vec<Event<T>>,
274}
275
276impl<T: Target> Default for EventLog<T> {
277    fn default() -> Self {
278        Self {
279            events: boxcar::Vec::new(),
280        }
281    }
282}
283
284impl<T: Target> fmt::Debug for EventLog<T> {
285    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
286        f.debug_struct("EventLog")
287            .field("len", &self.len())
288            .finish()
289    }
290}
291
292impl<T: Target> Clone for EventLog<T> {
293    fn clone(&self) -> Self {
294        let new_log = Self::new();
295        for (_, event) in &self.events {
296            new_log.events.push(event.clone());
297        }
298        new_log
299    }
300}
301
302impl<T: Target> EventListener<T> for EventLog<T> {
303    fn push(&self, event: Event<T>) {
304        self.events.push(event);
305    }
306}
307
308impl<T: Target> EventLog<T> {
309    /// Creates an empty event log.
310    #[must_use]
311    pub fn new() -> Self {
312        Self::default()
313    }
314
315    /// Open a fluent builder for an event of `kind`. Mirrors
316    /// [`EventListener::record`] as an inherent method so callers don't have
317    /// to import the trait. The event is appended when the builder is
318    /// dropped.
319    pub fn record(&self, kind: EventKind) -> EventBuilder<'_, T, Self> {
320        EventBuilder::new(self, kind)
321    }
322
323    /// Returns true if no events have been logged.
324    #[must_use]
325    pub fn is_empty(&self) -> bool {
326        self.events.count() == 0
327    }
328
329    /// Returns the total number of events.
330    #[must_use]
331    pub fn len(&self) -> usize {
332        self.events.count()
333    }
334
335    /// Merges another event log into this one.
336    pub fn merge(&self, other: &EventLog<T>) {
337        for (_, event) in &other.events {
338            self.events.push(event.clone());
339        }
340    }
341
342    /// Returns true if any event of the given kind exists.
343    #[must_use]
344    pub fn has(&self, kind: EventKind) -> bool {
345        self.events.iter().any(|(_, e)| e.kind == kind)
346    }
347
348    /// Returns true if any of the given event kinds exist.
349    #[must_use]
350    pub fn has_any(&self, kinds: &[EventKind]) -> bool {
351        self.events.iter().any(|(_, e)| kinds.contains(&e.kind))
352    }
353
354    /// Counts events of the given kind.
355    #[must_use]
356    pub fn count_kind(&self, kind: EventKind) -> usize {
357        self.events.iter().filter(|(_, e)| e.kind == kind).count()
358    }
359
360    /// Returns an iterator over all events.
361    pub fn iter(&self) -> impl Iterator<Item = &Event<T>> {
362        self.events.iter().map(|(_, e)| e)
363    }
364
365    /// Returns an iterator over events of a specific kind.
366    pub fn filter_kind(&self, kind: EventKind) -> impl Iterator<Item = &Event<T>> + '_ {
367        self.events
368            .iter()
369            .filter_map(move |(_, e)| if e.kind == kind { Some(e) } else { None })
370    }
371
372    /// Takes ownership of the events by cloning into a new EventLog.
373    ///
374    /// This is useful when the context is being consumed and you need to
375    /// extract the events. Since `boxcar::Vec` is append-only and doesn't
376    /// support draining, this creates a clone.
377    #[must_use]
378    pub fn take(&self) -> EventLog<T> {
379        self.clone()
380    }
381
382    /// Returns an iterator over events for a specific method.
383    pub fn filter_method<'a>(
384        &'a self,
385        method: &'a T::MethodRef,
386    ) -> impl Iterator<Item = &'a Event<T>> + 'a {
387        self.events.iter().filter_map(move |(_, e)| {
388            if e.method.as_ref() == Some(method) {
389                Some(e)
390            } else {
391                None
392            }
393        })
394    }
395
396    /// Returns an iterator over transformation events only.
397    pub fn transformations(&self) -> impl Iterator<Item = &Event<T>> + '_ {
398        self.events.iter().filter_map(|(_, e)| {
399            if e.kind.is_transformation() {
400                Some(e)
401            } else {
402                None
403            }
404        })
405    }
406
407    /// Counts events grouped by kind.
408    #[must_use]
409    pub fn count_by_kind(&self) -> HashMap<EventKind, usize> {
410        let mut counts: HashMap<EventKind, usize> = HashMap::new();
411        for (_, event) in &self.events {
412            let entry = counts.entry(event.kind).or_insert(0);
413            *entry = entry.saturating_add(1);
414        }
415        counts
416    }
417
418    /// Counts events grouped by kind, starting from the given offset.
419    ///
420    /// Used by the scheduler to compute per-pass event deltas without
421    /// iterating the entire log.
422    #[must_use]
423    pub fn count_by_kind_since(&self, offset: usize) -> HashMap<EventKind, usize> {
424        let mut counts: HashMap<EventKind, usize> = HashMap::new();
425        for (idx, event) in &self.events {
426            if idx >= offset {
427                let entry = counts.entry(event.kind).or_insert(0);
428                *entry = entry.saturating_add(1);
429            }
430        }
431        counts
432    }
433
434    /// Returns the number of transformation events.
435    #[must_use]
436    pub fn transformation_count(&self) -> usize {
437        self.events
438            .iter()
439            .filter(|(_, e)| e.kind.is_transformation())
440            .count()
441    }
442
443    /// Returns the number of unique methods with events.
444    #[must_use]
445    pub fn methods_affected(&self) -> usize {
446        self.events
447            .iter()
448            .filter_map(|(_, e)| e.method.as_ref())
449            .collect::<HashSet<_>>()
450            .len()
451    }
452
453    /// Generates a human-readable summary of all events.
454    #[must_use]
455    pub fn summary(&self) -> String {
456        if self.is_empty() {
457            return "no events".to_string();
458        }
459
460        let counts = self.count_by_kind();
461
462        let mut parts: Vec<String> = counts
463            .iter()
464            .filter(|(k, _)| k.is_transformation())
465            .map(|(kind, count)| format!("{} {}", count, kind.description()))
466            .collect();
467
468        if parts.is_empty() {
469            return format!("{} events", self.len());
470        }
471
472        parts.sort();
473        parts.join(", ")
474    }
475}
476
477/// Iterator wrapper for [`EventLog`] that yields `&Event<T>`.
478pub struct EventLogIter<'a, T: Target> {
479    inner: boxcar::Iter<'a, Event<T>>,
480}
481
482impl<'a, T: Target> Iterator for EventLogIter<'a, T> {
483    type Item = &'a Event<T>;
484
485    fn next(&mut self) -> Option<Self::Item> {
486        self.inner.next().map(|(_, e)| e)
487    }
488}
489
490impl<'a, T: Target> IntoIterator for &'a EventLog<T> {
491    type Item = &'a Event<T>;
492    type IntoIter = EventLogIter<'a, T>;
493
494    fn into_iter(self) -> Self::IntoIter {
495        EventLogIter {
496            inner: self.events.iter(),
497        }
498    }
499}
500
501impl<T: Target> Extend<Event<T>> for EventLog<T> {
502    fn extend<I: IntoIterator<Item = Event<T>>>(&mut self, iter: I) {
503        for event in iter {
504            self.events.push(event);
505        }
506    }
507}
508
509impl<T: Target> FromIterator<Event<T>> for EventLog<T> {
510    fn from_iter<I: IntoIterator<Item = Event<T>>>(iter: I) -> Self {
511        let log = Self::new();
512        for event in iter {
513            log.events.push(event);
514        }
515        log
516    }
517}
518
519/// Statistics derived from an [`EventLog`]. Counts are by [`EventKind`] and
520/// independent of `T`, so this struct is not generic.
521#[derive(Debug, Clone, Default)]
522pub struct DerivedStats {
523    /// Number of methods that had any transformations.
524    pub methods_transformed: usize,
525    /// Number of strings decrypted.
526    pub strings_decrypted: usize,
527    /// Number of arrays decrypted.
528    pub arrays_decrypted: usize,
529    /// Number of constants folded.
530    pub constants_folded: usize,
531    /// Number of constants decrypted.
532    pub constants_decrypted: usize,
533    /// Number of instructions removed.
534    pub instructions_removed: usize,
535    /// Number of blocks removed.
536    pub blocks_removed: usize,
537    /// Number of branches simplified.
538    pub branches_simplified: usize,
539    /// Number of opaque predicates removed.
540    pub opaque_predicates_removed: usize,
541    /// Number of methods inlined.
542    pub methods_inlined: usize,
543    /// Number of methods marked dead.
544    pub methods_marked_dead: usize,
545    /// Number of methods with code regenerated.
546    pub methods_regenerated: usize,
547    /// Number of artifacts removed (methods, types, metadata).
548    pub artifacts_removed: usize,
549    /// Number of pass iterations.
550    pub iterations: usize,
551    /// Processing time.
552    pub total_time: Duration,
553}
554
555impl DerivedStats {
556    /// Computes statistics from an event log.
557    #[must_use]
558    pub fn from_log<T: Target>(log: &EventLog<T>) -> Self {
559        let counts = log.count_by_kind();
560        let get = |kind: EventKind| counts.get(&kind).copied().unwrap_or(0);
561
562        Self {
563            methods_transformed: log.methods_affected(),
564            strings_decrypted: get(EventKind::StringDecrypted),
565            arrays_decrypted: get(EventKind::ArrayDecrypted),
566            constants_folded: get(EventKind::ConstantFolded),
567            constants_decrypted: get(EventKind::ConstantDecrypted),
568            instructions_removed: get(EventKind::InstructionRemoved),
569            blocks_removed: get(EventKind::BlockRemoved),
570            branches_simplified: get(EventKind::BranchSimplified),
571            opaque_predicates_removed: get(EventKind::OpaquePredicateRemoved),
572            methods_inlined: get(EventKind::MethodInlined),
573            methods_marked_dead: get(EventKind::MethodMarkedDead),
574            methods_regenerated: get(EventKind::CodeRegenerated),
575            artifacts_removed: get(EventKind::ArtifactRemoved),
576            iterations: 0,
577            total_time: Duration::ZERO,
578        }
579    }
580
581    /// Sets the total processing time.
582    #[must_use]
583    pub fn with_time(mut self, time: Duration) -> Self {
584        self.total_time = time;
585        self
586    }
587
588    /// Sets the number of iterations.
589    #[must_use]
590    pub fn with_iterations(mut self, iterations: usize) -> Self {
591        self.iterations = iterations;
592        self
593    }
594
595    /// Generates a human-readable summary.
596    #[must_use]
597    pub fn summary(&self) -> String {
598        let mut parts = Vec::new();
599
600        if self.methods_transformed > 0 {
601            parts.push(format!("{} methods", self.methods_transformed));
602        }
603
604        if self.strings_decrypted > 0 {
605            parts.push(format!("{} strings decrypted", self.strings_decrypted));
606        }
607        if self.arrays_decrypted > 0 {
608            parts.push(format!("{} arrays decrypted", self.arrays_decrypted));
609        }
610        if self.constants_decrypted > 0 {
611            parts.push(format!("{} constants decrypted", self.constants_decrypted));
612        }
613
614        if self.constants_folded > 0 {
615            parts.push(format!("{} constants folded", self.constants_folded));
616        }
617        if self.instructions_removed > 0 {
618            parts.push(format!(
619                "{} instructions removed",
620                self.instructions_removed
621            ));
622        }
623        if self.blocks_removed > 0 {
624            parts.push(format!("{} blocks removed", self.blocks_removed));
625        }
626        if self.branches_simplified > 0 {
627            parts.push(format!("{} branches simplified", self.branches_simplified));
628        }
629        if self.methods_inlined > 0 {
630            parts.push(format!("{} inlined", self.methods_inlined));
631        }
632        if self.opaque_predicates_removed > 0 {
633            parts.push(format!(
634                "{} opaque predicates",
635                self.opaque_predicates_removed
636            ));
637        }
638
639        if self.methods_marked_dead > 0 {
640            parts.push(format!("{} dead methods", self.methods_marked_dead));
641        }
642        if self.methods_regenerated > 0 {
643            parts.push(format!("{} regenerated", self.methods_regenerated));
644        }
645        if self.artifacts_removed > 0 {
646            parts.push(format!("{} artifacts removed", self.artifacts_removed));
647        }
648
649        let stats = if parts.is_empty() {
650            "no transformations".to_string()
651        } else {
652            parts.join(", ")
653        };
654
655        if self.total_time.as_millis() > 0 {
656            format!(
657                "{} in {:?} ({} iterations)",
658                stats, self.total_time, self.iterations
659            )
660        } else {
661            stats
662        }
663    }
664}
665
666impl fmt::Display for DerivedStats {
667    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
668        f.write_str(&self.summary())
669    }
670}
671
672/// Truncates a string for display, adding ellipsis if needed.
673#[must_use]
674pub fn truncate_string(s: &str, max_len: usize) -> String {
675    if s.len() <= max_len {
676        s.to_string()
677    } else {
678        let end = max_len.saturating_sub(3);
679        let split = s
680            .char_indices()
681            .map(|(i, _)| i)
682            .take_while(|&i| i <= end)
683            .last()
684            .unwrap_or(0);
685        format!("{}...", &s[..split])
686    }
687}
688
689#[cfg(test)]
690mod tests {
691    use super::*;
692
693    use std::{sync::Arc, thread};
694
695    use crate::testing::MockTarget;
696
697    type Method = <MockTarget as Target>::MethodRef;
698
699    fn method(id: u32) -> Method {
700        id
701    }
702
703    #[test]
704    fn empty_log() {
705        let log: EventLog<MockTarget> = EventLog::new();
706        assert!(log.is_empty());
707        assert_eq!(log.len(), 0);
708        assert!(!log.has(EventKind::StringDecrypted));
709    }
710
711    #[test]
712    fn record_event() {
713        let log: EventLog<MockTarget> = EventLog::new();
714        let m = method(0x06000001);
715
716        log.record(EventKind::StringDecrypted)
717            .at(m, 0x10)
718            .message("decrypted: \"hello\"");
719
720        assert!(!log.is_empty());
721        assert_eq!(log.len(), 1);
722        assert!(log.has(EventKind::StringDecrypted));
723
724        let event = log.iter().next().unwrap();
725        assert_eq!(event.method, Some(m));
726        assert_eq!(event.location, Some(0x10));
727        assert_eq!(event.message, "decrypted: \"hello\"");
728    }
729
730    #[test]
731    fn null_listener_discards() {
732        let listener = NullListener;
733        let m = method(0x06000001);
734
735        EventListener::<MockTarget>::record(&listener, EventKind::StringDecrypted)
736            .at(m, 0x10)
737            .message("dropped on the floor");
738
739        // No public observation surface — the test is that nothing panics
740        // and the listener compiles.
741    }
742
743    #[test]
744    fn multiple_events() {
745        let log: EventLog<MockTarget> = EventLog::new();
746        let m = method(0x06000001);
747
748        log.record(EventKind::StringDecrypted)
749            .at(m, 0x10)
750            .message("first");
751        log.record(EventKind::ConstantFolded)
752            .at(m, 0x20)
753            .message("second");
754
755        assert_eq!(log.len(), 2);
756        assert!(log.has(EventKind::StringDecrypted));
757        assert!(log.has(EventKind::ConstantFolded));
758        assert!(!log.has(EventKind::BlockRemoved));
759    }
760
761    #[test]
762    fn has_any() {
763        let log: EventLog<MockTarget> = EventLog::new();
764        log.record(EventKind::StringDecrypted)
765            .at(method(0x06000001), 0x10);
766
767        assert!(log.has_any(&[EventKind::StringDecrypted, EventKind::ArrayDecrypted]));
768        assert!(!log.has_any(&[EventKind::BlockRemoved, EventKind::MethodInlined]));
769    }
770
771    #[test]
772    fn merge() {
773        let log1: EventLog<MockTarget> = EventLog::new();
774        let log2: EventLog<MockTarget> = EventLog::new();
775        let m = method(0x06000001);
776
777        log1.record(EventKind::StringDecrypted).at(m, 0x10);
778        log2.record(EventKind::ConstantFolded).at(m, 0x20);
779
780        log1.merge(&log2);
781
782        assert_eq!(log1.len(), 2);
783        assert!(log1.has(EventKind::StringDecrypted));
784        assert!(log1.has(EventKind::ConstantFolded));
785    }
786
787    #[test]
788    fn summary() {
789        let log: EventLog<MockTarget> = EventLog::new();
790        let m = method(0x06000001);
791
792        log.record(EventKind::StringDecrypted).at(m, 0x10);
793        log.record(EventKind::StringDecrypted).at(m, 0x20);
794        log.record(EventKind::ConstantFolded).at(m, 0x30);
795
796        let summary = log.summary();
797        assert!(summary.contains("2 string decrypted"));
798        assert!(summary.contains("1 constant folded"));
799    }
800
801    #[test]
802    fn count_by_kind() {
803        let log: EventLog<MockTarget> = EventLog::new();
804        let m = method(0x06000001);
805
806        log.record(EventKind::StringDecrypted).at(m, 0x10);
807        log.record(EventKind::StringDecrypted).at(m, 0x20);
808        log.record(EventKind::ConstantFolded).at(m, 0x30);
809
810        let counts = log.count_by_kind();
811        assert_eq!(counts.get(&EventKind::StringDecrypted), Some(&2));
812        assert_eq!(counts.get(&EventKind::ConstantFolded), Some(&1));
813        assert_eq!(counts.get(&EventKind::BlockRemoved), None);
814    }
815
816    #[test]
817    fn count_by_kind_since() {
818        let log: EventLog<MockTarget> = EventLog::new();
819        let m = method(0x06000001);
820
821        log.record(EventKind::StringDecrypted).at(m, 0x10);
822        log.record(EventKind::StringDecrypted).at(m, 0x20);
823
824        let offset = log.len();
825
826        log.record(EventKind::ConstantFolded).at(m, 0x30);
827        log.record(EventKind::ConstantFolded).at(m, 0x40);
828        log.record(EventKind::StringDecrypted).at(m, 0x50);
829
830        let counts = log.count_by_kind_since(offset);
831        assert_eq!(counts.get(&EventKind::ConstantFolded), Some(&2));
832        assert_eq!(counts.get(&EventKind::StringDecrypted), Some(&1));
833        assert_eq!(counts.get(&EventKind::BlockRemoved), None);
834
835        let all = log.count_by_kind_since(0);
836        assert_eq!(all.get(&EventKind::StringDecrypted), Some(&3));
837        assert_eq!(all.get(&EventKind::ConstantFolded), Some(&2));
838    }
839
840    #[test]
841    fn derived_stats() {
842        let log: EventLog<MockTarget> = EventLog::new();
843        let m1 = method(0x06000001);
844        let m2 = method(0x06000002);
845
846        log.record(EventKind::StringDecrypted).at(m1, 0x10);
847        log.record(EventKind::StringDecrypted).at(m2, 0x20);
848        log.record(EventKind::ConstantFolded).at(m1, 0x30);
849
850        let stats = DerivedStats::from_log(&log);
851        assert_eq!(stats.methods_transformed, 2);
852        assert_eq!(stats.strings_decrypted, 2);
853        assert_eq!(stats.constants_folded, 1);
854    }
855
856    #[test]
857    fn filter_methods() {
858        let log: EventLog<MockTarget> = EventLog::new();
859        let m1 = method(0x06000001);
860        let m2 = method(0x06000002);
861
862        log.record(EventKind::StringDecrypted).at(m1, 0x10);
863        log.record(EventKind::ConstantFolded).at(m2, 0x20);
864        log.record(EventKind::BlockRemoved).at(m1, 0x30);
865
866        let m1_events: Vec<_> = log.filter_method(&m1).collect();
867        assert_eq!(m1_events.len(), 2);
868    }
869
870    #[test]
871    fn transformations_filter() {
872        let log: EventLog<MockTarget> = EventLog::new();
873        let m = method(0x06000001);
874
875        log.record(EventKind::StringDecrypted).at(m, 0x10);
876        log.record(EventKind::BlockRemoved).at(m, 0x20);
877
878        let transformations: Vec<_> = log.transformations().collect();
879        assert_eq!(transformations.len(), 2);
880    }
881
882    #[test]
883    fn event_with_pass() {
884        let log: EventLog<MockTarget> = EventLog::new();
885        let m = method(0x06000001);
886
887        log.record(EventKind::ConstantFolded)
888            .at(m, 0x10)
889            .pass("ConstantFolding")
890            .message("42 + 0 → 42");
891
892        let event = log.iter().next().unwrap();
893        assert_eq!(event.pass.as_deref(), Some("ConstantFolding"));
894    }
895
896    #[test]
897    fn default_message() {
898        let log: EventLog<MockTarget> = EventLog::new();
899        let m = method(0x06000001);
900
901        log.record(EventKind::StringDecrypted).at(m, 0x10);
902
903        let event = log.iter().next().unwrap();
904        assert_eq!(event.message, "string decrypted");
905    }
906
907    #[test]
908    fn thread_safe_append() {
909        let log: Arc<EventLog<MockTarget>> = Arc::new(EventLog::new());
910        let mut handles = vec![];
911
912        for i in 0..4u32 {
913            let log_clone = Arc::clone(&log);
914            handles.push(thread::spawn(move || {
915                for j in 0..100u32 {
916                    let m = method(
917                        0x06000000u32
918                            .saturating_add(i.saturating_mul(100))
919                            .saturating_add(j),
920                    );
921                    log_clone
922                        .record(EventKind::StringDecrypted)
923                        .at(m, j as usize)
924                        .message(format!("thread {} event {}", i, j));
925                }
926            }));
927        }
928
929        for handle in handles {
930            handle.join().unwrap();
931        }
932
933        assert_eq!(log.len(), 400);
934    }
935
936    #[test]
937    fn truncate_string_short() {
938        assert_eq!(truncate_string("hi", 10), "hi");
939    }
940
941    #[test]
942    fn truncate_string_long() {
943        let result = truncate_string("hello world", 8);
944        assert!(result.ends_with("..."));
945        assert!(result.len() <= 8);
946    }
947}