Skip to main content

jdwp_client/
events.rs

1// JDWP event handling
2//
3// Events are sent from the JVM to notify about breakpoints, steps, etc.
4
5use crate::commands::event_kinds;
6use crate::protocol::JdwpResult;
7use crate::reader::{read_i32, read_string, read_u64, read_u8};
8use crate::types::{FieldId, Location, ObjectId, ReferenceTypeId, ThreadId, Value};
9use serde::{Deserialize, Serialize};
10use tracing::warn;
11
12/// Composite event packet (can contain multiple events)
13#[derive(Debug, Clone, Serialize, Deserialize)]
14pub struct EventSet {
15    pub suspend_policy: u8,
16    pub events: Vec<Event>,
17}
18
19/// Single event within an event set
20#[derive(Debug, Clone, Serialize, Deserialize)]
21pub struct Event {
22    pub kind: u8,
23    pub request_id: i32,
24    pub details: EventKind,
25}
26
27#[derive(Debug, Clone, Serialize, Deserialize)]
28#[serde(tag = "type")]
29pub enum EventKind {
30    VMStart {
31        thread: ThreadId,
32    },
33    VMDeath,
34    ThreadStart {
35        thread: ThreadId,
36    },
37    ThreadDeath {
38        thread: ThreadId,
39    },
40    ClassPrepare {
41        thread: ThreadId,
42        ref_type: ReferenceTypeId,
43        signature: String,
44        status: i32,
45    },
46    Breakpoint {
47        thread: ThreadId,
48        location: Location,
49    },
50    Step {
51        thread: ThreadId,
52        location: Location,
53    },
54    Exception {
55        thread: ThreadId,
56        location: Location,
57        exception: ObjectId,
58        catch_location: Option<Location>,
59    },
60    /// A method is returning. `location` is the return site, so a method with several `return`
61    /// statements says which one was taken.
62    ///
63    /// There is deliberately no `MethodEntry`: a `METHOD_ENTRY` request with a `ClassMatch` fires on
64    /// every method of every matching class — the noisiest event in JDWP — and "what calls this?" is
65    /// now answered far more cheaply by a traced breakpoint's caller chain (TRACE-5). A decoded variant
66    /// nothing can arm only implies a capability that isn't there.
67    MethodExit {
68        thread: ThreadId,
69        location: Location,
70        /// What the method is returning, present only when the request was armed as
71        /// `METHOD_EXIT_WITH_RETURN_VALUE` (kind 42). `None` for a plain `METHOD_EXIT` (kind 41),
72        /// which a JVM below JDWP 1.6 is all you can get.
73        return_value: Option<Value>,
74    },
75    /// A watched field was read.
76    FieldAccess {
77        field: FieldEvent,
78    },
79    /// A watched field is about to be written. The event fires *before* the store commits, so the
80    /// field still holds its old value while the thread is suspended — that is how the old→new pair
81    /// is reported.
82    FieldModification {
83        field: FieldEvent,
84        /// JDWP's `valueToBe` — the value the write will store.
85        new_value: Value,
86    },
87    /// A thread has begun **blocking** on a monitor another thread owns
88    /// (`MONITOR_CONTENDED_ENTER`, 43). The thread is off the pool from here until the matching
89    /// [`MonitorContendedEntered`](Self::MonitorContendedEntered) arrives.
90    MonitorContendedEnter {
91        monitor: MonitorEvent,
92    },
93    /// A thread that was blocking has **acquired** the monitor (`MONITOR_CONTENDED_ENTERED`, 44).
94    ///
95    /// **This event carries no timing of any kind.** How long the thread was blocked — the actual
96    /// question a contention diagnosis asks — is on neither half of the pair, so it can only be had by
97    /// timestamping the `ENTER` on this side and matching it here. See `mcp-server`'s monitor pairing and
98    /// ADR-0035: the resulting figure is a *debugger* measurement and every reply that prints one says so.
99    MonitorContendedEntered {
100        monitor: MonitorEvent,
101    },
102    /// A thread is about to `Object.wait()` (`MONITOR_WAIT`, 45).
103    MonitorWait {
104        monitor: MonitorEvent,
105        /// JDWP's `timeout` — the number of milliseconds the caller **asked** `wait(…)` for, `0` for an
106        /// untimed wait. It is the argument, not a measurement: a `wait(5000)` that returns after 3 ms
107        /// still reports 5000 here.
108        timeout: i64,
109    },
110    /// A thread's `Object.wait()` has returned (`MONITOR_WAITED`, 46).
111    MonitorWaited {
112        monitor: MonitorEvent,
113        /// Whether the wait ended because the timeout expired rather than because of a `notify`. The one
114        /// piece of outcome the wire does carry, and the difference between "nobody signalled it" and
115        /// "it was signalled" — which are opposite diagnoses.
116        timed_out: bool,
117    },
118    Unknown {
119        kind: u8,
120    },
121}
122
123/// Which monitor was contended, by which thread, at what code.
124///
125/// The context all four monitor events carry. They differ only in what (if anything) follows it, so it
126/// lives in one struct, exactly as [`FieldEvent`] does for the two field events.
127#[derive(Debug, Clone, Serialize, Deserialize)]
128pub struct MonitorEvent {
129    /// The thread that blocked, acquired, waited or finished waiting.
130    pub thread: ThreadId,
131    /// The code that was executing, **not** where the monitor's type is declared — for a
132    /// `synchronized` block, the block's own location.
133    pub location: Location,
134    /// The monitor object itself. Arrives as a tagged-objectID and is a **weak** reference like every
135    /// other object id here (ADR-0022), so a pairing keyed on it must not assume it stays readable.
136    pub monitor: ObjectId,
137}
138
139/// Which field was touched, by what code, on which object — the context both field events carry.
140/// They differ only in whether a pending value comes with it, so it lives in one struct.
141#[derive(Debug, Clone, Serialize, Deserialize)]
142pub struct FieldEvent {
143    pub thread: ThreadId,
144    /// The code that touched the field, *not* where the field is declared.
145    pub location: Location,
146    /// The type declaring the field.
147    pub ref_type: ReferenceTypeId,
148    pub field_id: FieldId,
149    /// The instance whose field was touched; 0 for a static field.
150    pub object: ObjectId,
151}
152
153// Event request modifiers
154#[derive(Debug, Clone)]
155pub enum EventModifier {
156    Count(i32),
157    ThreadOnly(ThreadId),
158    ClassOnly(ReferenceTypeId),
159    ClassMatch(String),
160    ClassExclude(String),
161    LocationOnly(Location),
162    ExceptionOnly { ref_type: ReferenceTypeId, caught: bool, uncaught: bool },
163    FieldOnly { ref_type: ReferenceTypeId, field_id: FieldId },
164    Step { thread: ThreadId, size: i32, depth: i32 },
165    InstanceOnly(ObjectId),
166}
167
168/// Parse an event packet from JDWP
169///
170/// # Errors
171/// Returns a [`JdwpError`](crate::JdwpError) if the buffer does not contain enough bytes or is malformed.
172pub fn parse_event_packet(data: &[u8]) -> JdwpResult<EventSet> {
173    let mut buf = data;
174
175    // Read suspend policy
176    let suspend_policy = read_u8(&mut buf)?;
177
178    // Read number of events
179    let event_count = read_i32(&mut buf)?;
180
181    let mut events = Vec::with_capacity(usize::try_from(event_count).unwrap_or(0));
182
183    for _ in 0..event_count {
184        let kind = read_u8(&mut buf)?;
185        let request_id = read_i32(&mut buf)?;
186
187        let details = parse_event_details(kind, &mut buf)?;
188
189        events.push(Event { kind, request_id, details });
190    }
191
192    Ok(EventSet { suspend_policy, events })
193}
194
195/// Dispatch a single event's kind-specific payload to the matching parser.
196///
197/// The **stop-point** kinds are here — the ones a debugger arms deliberately and that carry a thread and a
198/// location — while the VM's own lifecycle notifications and the monitor family are delegated. Split that
199/// way because the table had grown past the point where one `match` could be read at a glance, and because
200/// those are the two groups whose members share a shape: [`parse_vm_lifecycle_event`]'s carry no location,
201/// and [`parse_monitor_event`]'s all share one prefix.
202fn parse_event_details(kind: u8, buf: &mut &[u8]) -> JdwpResult<EventKind> {
203    if let Some(parsed) = parse_vm_lifecycle_event(kind, buf) {
204        return parsed;
205    }
206    if let Some(parsed) = parse_monitor_event(kind, buf) {
207        return parsed;
208    }
209    match kind {
210        event_kinds::BREAKPOINT => parse_breakpoint_event(buf),
211        event_kinds::SINGLE_STEP => parse_step_event(buf),
212        event_kinds::EXCEPTION => parse_exception_event(buf),
213        event_kinds::FIELD_ACCESS => parse_field_access_event(buf),
214        event_kinds::FIELD_MODIFICATION => parse_field_modification_event(buf),
215        event_kinds::METHOD_EXIT => parse_method_exit_event(buf, false),
216        event_kinds::METHOD_EXIT_WITH_RETURN_VALUE => parse_method_exit_event(buf, true),
217        _ => {
218            warn!("Unsupported event kind: {}", kind);
219            Ok(EventKind::Unknown { kind })
220        }
221    }
222}
223
224/// The VM's own lifecycle notifications, which arrive whether anything asked for them or not. `None` for
225/// any other kind, so the caller keeps dispatching.
226///
227/// These share a shape: none of them carries a location, which is why `event_location` on the `mcp-server`
228/// side answers `None` for every one of them.
229fn parse_vm_lifecycle_event(kind: u8, buf: &mut &[u8]) -> Option<JdwpResult<EventKind>> {
230    match kind {
231        event_kinds::VM_START => Some(parse_vm_start_event(buf)),
232        event_kinds::VM_DEATH => Some(Ok(EventKind::VMDeath)),
233        event_kinds::THREAD_START => Some(parse_thread_start_event(buf)),
234        event_kinds::THREAD_DEATH => Some(parse_thread_death_event(buf)),
235        event_kinds::CLASS_PREPARE => Some(parse_class_prepare_event(buf)),
236        _ => None,
237    }
238}
239
240/// The four monitor kinds (DUMP-7, #96). `None` for any other kind.
241///
242/// Grouped because they share [`parse_monitor_event_head`] — the tagged-objectID prefix and its trap — and
243/// differ only in a trailing field of a different Rust type each.
244fn parse_monitor_event(kind: u8, buf: &mut &[u8]) -> Option<JdwpResult<EventKind>> {
245    match kind {
246        event_kinds::MONITOR_CONTENDED_ENTER => {
247            Some(parse_monitor_event_head(buf).map(|monitor| EventKind::MonitorContendedEnter { monitor }))
248        }
249        event_kinds::MONITOR_CONTENDED_ENTERED => {
250            Some(parse_monitor_event_head(buf).map(|monitor| EventKind::MonitorContendedEntered { monitor }))
251        }
252        event_kinds::MONITOR_WAIT => Some(parse_monitor_wait_event(buf)),
253        event_kinds::MONITOR_WAITED => Some(parse_monitor_waited_event(buf)),
254        _ => None,
255    }
256}
257
258fn parse_breakpoint_event(buf: &mut &[u8]) -> JdwpResult<EventKind> {
259    let thread = read_u64(buf)?;
260    let location = read_location(buf)?;
261    Ok(EventKind::Breakpoint { thread, location })
262}
263
264fn parse_step_event(buf: &mut &[u8]) -> JdwpResult<EventKind> {
265    let thread = read_u64(buf)?;
266    let location = read_location(buf)?;
267    Ok(EventKind::Step { thread, location })
268}
269
270fn parse_vm_start_event(buf: &mut &[u8]) -> JdwpResult<EventKind> {
271    let thread = read_u64(buf)?;
272    Ok(EventKind::VMStart { thread })
273}
274
275fn parse_thread_start_event(buf: &mut &[u8]) -> JdwpResult<EventKind> {
276    let thread = read_u64(buf)?;
277    Ok(EventKind::ThreadStart { thread })
278}
279
280fn parse_thread_death_event(buf: &mut &[u8]) -> JdwpResult<EventKind> {
281    let thread = read_u64(buf)?;
282    Ok(EventKind::ThreadDeath { thread })
283}
284
285fn parse_class_prepare_event(buf: &mut &[u8]) -> JdwpResult<EventKind> {
286    // thread, refTypeTag (byte, discarded), typeID, signature, status
287    let thread = read_u64(buf)?;
288    let _ref_type_tag = read_u8(buf)?;
289    let ref_type = read_u64(buf)?;
290    let signature = read_string(buf)?;
291    let status = read_i32(buf)?;
292    Ok(EventKind::ClassPrepare { thread, ref_type, signature, status })
293}
294
295fn parse_exception_event(buf: &mut &[u8]) -> JdwpResult<EventKind> {
296    // thread, throw location, exception (tagged-objectID), catch location.
297    // The catch location is all-zero when the exception is uncaught.
298    let thread = read_u64(buf)?;
299    let location = read_location(buf)?;
300    let _exc_tag = read_u8(buf)?;
301    let exception = read_u64(buf)?;
302    let catch = read_location(buf)?;
303    let catch_location =
304        if catch.class_id == 0 && catch.method_id == 0 && catch.index == 0 { None } else { Some(catch) };
305    Ok(EventKind::Exception { thread, location, exception, catch_location })
306}
307
308/// Read the prefix both field events share: thread, the touching location, the declaring type, the
309/// field, and the instance involved (a tagged-objectID that is null for a static field).
310fn parse_field_event_head(buf: &mut &[u8]) -> JdwpResult<FieldEvent> {
311    let thread = read_u64(buf)?;
312    let location = read_location(buf)?;
313    let _ref_type_tag = read_u8(buf)?;
314    let ref_type = read_u64(buf)?;
315    let field_id = read_u64(buf)?;
316    let _obj_tag = read_u8(buf)?;
317    let object = read_u64(buf)?;
318    Ok(FieldEvent { thread, location, ref_type, field_id, object })
319}
320
321fn parse_field_access_event(buf: &mut &[u8]) -> JdwpResult<EventKind> {
322    Ok(EventKind::FieldAccess { field: parse_field_event_head(buf)? })
323}
324
325fn parse_field_modification_event(buf: &mut &[u8]) -> JdwpResult<EventKind> {
326    let field = parse_field_event_head(buf)?;
327    // valueToBe: a tagged value carrying what the pending write will store.
328    let tag = read_u8(buf)?;
329    let new_value = Value { tag, data: crate::reader::read_value_by_tag(tag, buf)? };
330    Ok(EventKind::FieldModification { field, new_value })
331}
332
333/// Read the prefix all four monitor events share: the thread, the monitor object (a tagged-objectID),
334/// and the location of the code involved.
335///
336/// **Note the field ORDER, which is not the field events' order.** A monitor event puts its object
337/// *before* the location; `parse_field_event_head` above puts the location first. Getting it the other
338/// way round does not fail — a location's leading typeTag byte reads as the object's tag and the whole
339/// remainder shifts, which for [`parse_monitor_wait_event`] means a garbage timeout and inside a
340/// composite means the *next* event desynchronises.
341///
342/// One head parser rather than four copies, and rather than a shape enum: the trap this exists to
343/// contain is entirely in the shared prefix — the tag byte — while the two tails differ in Rust *type*
344/// (`i64` against `bool`), so an enum would only move the match one level out. This is the same split
345/// `parse_field_event_head` uses for the same reason.
346fn parse_monitor_event_head(buf: &mut &[u8]) -> JdwpResult<MonitorEvent> {
347    let thread = read_u64(buf)?;
348    // The monitor is a tagged-objectID: one tag byte (always `L`, an object) and then the id. Dropping
349    // the tag read is the mistake that silently shifts every field after it.
350    let _monitor_tag = read_u8(buf)?;
351    let monitor = read_u64(buf)?;
352    let location = read_location(buf)?;
353    Ok(MonitorEvent { thread, location, monitor })
354}
355
356fn parse_monitor_wait_event(buf: &mut &[u8]) -> JdwpResult<EventKind> {
357    let monitor = parse_monitor_event_head(buf)?;
358    // The timeout the caller passed to `wait(…)`, not how long it waited. Signed, because that is how
359    // JDWP declares it and how `Object.wait(long)` takes it.
360    let timeout = crate::reader::read_i64(buf)?;
361    Ok(EventKind::MonitorWait { monitor, timeout })
362}
363
364fn parse_monitor_waited_event(buf: &mut &[u8]) -> JdwpResult<EventKind> {
365    let monitor = parse_monitor_event_head(buf)?;
366    let timed_out = read_u8(buf)? != 0;
367    Ok(EventKind::MonitorWaited { monitor, timed_out })
368}
369
370/// Parse a `METHOD_EXIT` (kind 41) or `METHOD_EXIT_WITH_RETURN_VALUE` (kind 42) event.
371///
372/// The two differ only by a trailing tagged value, so `with_return_value` decides whether to read it.
373/// Reading it when the request did not ask for it would consume the next event's bytes.
374fn parse_method_exit_event(buf: &mut &[u8], with_return_value: bool) -> JdwpResult<EventKind> {
375    let thread = read_u64(buf)?;
376    let location = read_location(buf)?;
377    let return_value = if with_return_value {
378        let tag = read_u8(buf)?;
379        Some(Value { tag, data: crate::reader::read_value_by_tag(tag, buf)? })
380    } else {
381        None
382    };
383    Ok(EventKind::MethodExit { thread, location, return_value })
384}
385
386/// Read a location from the buffer
387fn read_location(buf: &mut &[u8]) -> JdwpResult<Location> {
388    let type_tag = read_u8(buf)?;
389    let class_id = read_u64(buf)?;
390    let method_id = read_u64(buf)?;
391    let index = read_u64(buf)?;
392
393    Ok(Location { type_tag, class_id, method_id, index })
394}
395
396#[cfg(test)]
397mod tests {
398    use super::*;
399    use crate::commands::event_kinds;
400
401    /// Build an event-packet body: suspend policy, event count, then each event's bytes.
402    fn packet(suspend_policy: u8, events: &[Vec<u8>]) -> Vec<u8> {
403        let mut out = vec![suspend_policy];
404        out.extend_from_slice(&i32::try_from(events.len()).unwrap_or(0).to_be_bytes());
405        for e in events {
406            out.extend_from_slice(e);
407        }
408        out
409    }
410
411    /// A JDWP location: typeTag, classID, methodID, code index.
412    fn location(class: u64, method: u64, index: u64) -> Vec<u8> {
413        let mut out = vec![1];
414        out.extend_from_slice(&class.to_be_bytes());
415        out.extend_from_slice(&method.to_be_bytes());
416        out.extend_from_slice(&index.to_be_bytes());
417        out
418    }
419
420    fn breakpoint_event(request_id: i32, thread: u64) -> Vec<u8> {
421        let mut out = vec![event_kinds::BREAKPOINT];
422        out.extend_from_slice(&request_id.to_be_bytes());
423        out.extend_from_slice(&thread.to_be_bytes());
424        out.extend_from_slice(&location(0x11, 0x22, 3));
425        out
426    }
427
428    /// A `FIELD_MODIFICATION` event, whose trailing `valueToBe` is the one place an event parser reads a
429    /// tagged value — and the path that used to panic on a short buffer instead of erroring.
430    fn field_modification_event(new_value: i32) -> Vec<u8> {
431        let mut out = vec![event_kinds::FIELD_MODIFICATION];
432        out.extend_from_slice(&7i32.to_be_bytes()); // requestId
433        out.extend_from_slice(&0x1u64.to_be_bytes()); // thread
434        out.extend_from_slice(&location(0x11, 0x22, 3));
435        out.push(1); // refTypeTag
436        out.extend_from_slice(&0x33u64.to_be_bytes()); // refType
437        out.extend_from_slice(&0x44u64.to_be_bytes()); // fieldId
438        out.push(crate::reader::value_tags::OBJECT); // object tag
439        out.extend_from_slice(&0u64.to_be_bytes()); // object (0 = static field)
440        out.push(crate::reader::value_tags::INT);
441        out.extend_from_slice(&new_value.to_be_bytes());
442        out
443    }
444
445    /// A `METHOD_EXIT` (41) or `METHOD_EXIT_WITH_RETURN_VALUE` (42) event. Kind 42 carries a trailing
446    /// tagged value; kind 41 does not, and reading one anyway would eat the next event's bytes.
447    fn method_exit_event(with_return_value: bool, returned: i32) -> Vec<u8> {
448        let mut out = vec![if with_return_value {
449            event_kinds::METHOD_EXIT_WITH_RETURN_VALUE
450        } else {
451            event_kinds::METHOD_EXIT
452        }];
453        out.extend_from_slice(&9i32.to_be_bytes()); // requestId
454        out.extend_from_slice(&0x1u64.to_be_bytes()); // thread
455        out.extend_from_slice(&location(0x55, 0x66, 12));
456        if with_return_value {
457            out.push(crate::reader::value_tags::INT);
458            out.extend_from_slice(&returned.to_be_bytes());
459        }
460        out
461    }
462
463    /// METH-1: kind 42 yields the returned value; kind 41 yields the return site with no value. Getting
464    /// this wrong is not a missing field but a desynchronised buffer — the value's bytes would be read
465    /// as the next event's header.
466    #[test]
467    fn method_exit_parses_with_and_without_a_return_value() {
468        let with = parse_event_packet(&packet(1, &[method_exit_event(true, 42)])).expect("well-formed");
469        match with.events.first().map(|e| &e.details) {
470            Some(EventKind::MethodExit { location, return_value: Some(v), .. }) => {
471                assert_eq!(location.method_id, 0x66, "the return site is the hit location");
472                assert!(matches!(v.data, crate::types::ValueData::Int(42)), "got {:?}", v.data);
473            }
474            other => panic!("expected a method exit with a value, got {other:?}"),
475        }
476
477        let without = parse_event_packet(&packet(1, &[method_exit_event(false, 0)])).expect("well-formed");
478        assert!(
479            matches!(
480                without.events.first().map(|e| &e.details),
481                Some(EventKind::MethodExit { return_value: None, .. })
482            ),
483            "kind 41 carries no value, got {:?}",
484            without.events.first().map(|e| &e.details)
485        );
486
487        // Two kind-42 events back to back: the second only parses if the first consumed its value and
488        // nothing more. This is the assertion that catches a length mistake in the tagged-value read.
489        let pair = parse_event_packet(&packet(1, &[method_exit_event(true, 7), method_exit_event(true, 8)]))
490            .expect("well-formed");
491        assert_eq!(pair.events.len(), 2, "the first event must consume exactly its own bytes");
492    }
493
494    #[test]
495    fn an_empty_event_set_parses_as_zero_events() {
496        let set = parse_event_packet(&packet(2, &[])).expect("an empty set is well-formed");
497        assert_eq!(set.suspend_policy, 2);
498        assert!(set.events.is_empty());
499    }
500
501    #[test]
502    fn a_well_formed_set_parses_every_event() {
503        let wire = packet(1, &[breakpoint_event(5, 0xabc), field_modification_event(42)]);
504        let set = parse_event_packet(&wire).expect("well-formed");
505        assert_eq!(set.events.len(), 2);
506        match &set.events[0].details {
507            EventKind::Breakpoint { thread, location } => {
508                assert_eq!(*thread, 0xabc);
509                assert_eq!(location.method_id, 0x22);
510            }
511            other => panic!("expected a breakpoint, got {other:?}"),
512        }
513        match &set.events[1].details {
514            EventKind::FieldModification { field, new_value } => {
515                assert_eq!(field.field_id, 0x44);
516                assert!(matches!(new_value.data, crate::types::ValueData::Int(42)));
517            }
518            other => panic!("expected a field modification, got {other:?}"),
519        }
520    }
521
522    /// An event kind we don't parse must degrade to `Unknown`, not fail the whole set: the JVM sends
523    /// kinds we never requested (frame pops, class unloads), and dropping the set would lose
524    /// the events beside it.
525    #[test]
526    fn an_unhandled_event_kind_becomes_unknown_rather_than_an_error() {
527        // FRAME_POP is a real JDWP kind this client never requests and does not parse — the honest case,
528        // rather than a number the protocol doesn't define. It used to be `MONITOR_WAIT`, which DUMP-7
529        // (#96) decoded: an example chosen for being unhandled has to be re-chosen when it stops being.
530        let mut ev = vec![event_kinds::FRAME_POP];
531        ev.extend_from_slice(&1i32.to_be_bytes());
532        let set = parse_event_packet(&packet(0, &[ev])).expect("an unhandled kind is not a parse failure");
533        assert!(
534            matches!(set.events.first().map(|e| &e.details), Some(EventKind::Unknown { kind })
535                if *kind == event_kinds::FRAME_POP),
536            "expected Unknown, got {:?}",
537            set.events.first().map(|e| &e.details)
538        );
539    }
540
541    /// A monitor event of any of the four kinds: thread, the monitor as a **tagged**-objectID, location,
542    /// then whichever tail the kind carries.
543    fn monitor_event(kind: u8, monitor: u64, tail: &[u8]) -> Vec<u8> {
544        let mut out = vec![kind];
545        out.extend_from_slice(&11i32.to_be_bytes()); // requestId
546        out.extend_from_slice(&0x7fu64.to_be_bytes()); // thread
547        out.push(crate::reader::value_tags::OBJECT); // the tag byte the head parser must consume
548        out.extend_from_slice(&monitor.to_be_bytes());
549        out.extend_from_slice(&location(0x99, 0xaa, 4));
550        out.extend_from_slice(tail);
551        out
552    }
553
554    /// DUMP-7 (#96): all four kinds decode, and each reports the monitor object rather than reading the
555    /// location's typeTag as part of it.
556    ///
557    /// The two-in-one-packet assertions are the load-bearing ones. Dropping the tagged-objectID's tag
558    /// byte, or reading `MONITOR_WAIT`'s trailing `long` for a kind that does not carry one, both leave a
559    /// single event *looking* fine while shifting everything after it — so the mistake only shows up as a
560    /// second event that fails to parse or arrives with garbage.
561    #[test]
562    fn every_monitor_event_kind_decodes_with_its_own_tail() {
563        let enter = parse_event_packet(&packet(
564            1,
565            &[monitor_event(event_kinds::MONITOR_CONTENDED_ENTER, 0x1234, &[])],
566        ))
567        .expect("well-formed");
568        match enter.events.first().map(|e| &e.details) {
569            Some(EventKind::MonitorContendedEnter { monitor }) => {
570                assert_eq!(monitor.monitor, 0x1234, "the monitor object, not the location's typeTag");
571                assert_eq!(monitor.thread, 0x7f);
572                assert_eq!(monitor.location.method_id, 0xaa);
573            }
574            other => panic!("expected a contended enter, got {other:?}"),
575        }
576
577        // ENTER and ENTERED back to back: the second only parses if the first consumed exactly its own
578        // bytes — the pair a debugger-measured elapsed is computed from, so both halves must survive one
579        // composite.
580        let pair = parse_event_packet(&packet(
581            1,
582            &[
583                monitor_event(event_kinds::MONITOR_CONTENDED_ENTER, 0x1234, &[]),
584                monitor_event(event_kinds::MONITOR_CONTENDED_ENTERED, 0x1234, &[]),
585            ],
586        ))
587        .expect("well-formed");
588        assert_eq!(pair.events.len(), 2, "an enter must consume exactly its own bytes");
589        assert!(
590            matches!(&pair.events[1].details, EventKind::MonitorContendedEntered { monitor } if monitor.monitor == 0x1234),
591            "got {:?}",
592            pair.events[1].details
593        );
594
595        // WAIT's trailing `long` is the requested timeout, and WAITED's is a one-byte flag. A second
596        // event after each is what catches reading the wrong width.
597        let waits = parse_event_packet(&packet(
598            1,
599            &[
600                monitor_event(event_kinds::MONITOR_WAIT, 0x55, &5000i64.to_be_bytes()),
601                monitor_event(event_kinds::MONITOR_WAITED, 0x55, &[1]),
602                monitor_event(event_kinds::MONITOR_WAITED, 0x55, &[0]),
603            ],
604        ))
605        .expect("well-formed");
606        assert_eq!(waits.events.len(), 3, "each tail must be consumed at its own width");
607        assert!(
608            matches!(&waits.events[0].details, EventKind::MonitorWait { timeout: 5000, .. }),
609            "got {:?}",
610            waits.events[0].details
611        );
612        assert!(
613            matches!(&waits.events[1].details, EventKind::MonitorWaited { timed_out: true, .. }),
614            "got {:?}",
615            waits.events[1].details
616        );
617        assert!(
618            matches!(&waits.events[2].details, EventKind::MonitorWaited { timed_out: false, .. }),
619            "a notified wait did not time out, got {:?}",
620            waits.events[2].details
621        );
622    }
623
624    /// Every truncation of a valid packet must error rather than panic. This is the assertion that
625    /// would have caught the pre-`reader` behaviour: a short `valueToBe` panicked the event-loop task,
626    /// killing the whole debug session instead of reporting a malformed reply.
627    #[test]
628    fn every_truncation_of_a_packet_errors_instead_of_panicking() {
629        for event in [
630            breakpoint_event(5, 0xabc),
631            field_modification_event(42),
632            method_exit_event(true, 42),
633            monitor_event(event_kinds::MONITOR_WAIT, 0x55, &5000i64.to_be_bytes()),
634        ] {
635            let wire = packet(1, &[event]);
636            for keep in 0..wire.len() {
637                let short = &wire[..keep];
638                // The count field can read as 0 before it is complete, which is a legitimate empty set.
639                let parsed = parse_event_packet(short);
640                if let Ok(set) = parsed {
641                    assert!(
642                        set.events.is_empty(),
643                        "{keep} of {} bytes parsed as {} complete event(s)",
644                        wire.len(),
645                        set.events.len()
646                    );
647                }
648            }
649        }
650    }
651
652    /// A claimed event count far larger than the data must not pre-allocate for it or panic — it must
653    /// fail when the events run out.
654    #[test]
655    fn a_lying_event_count_errors_rather_than_over_reading() {
656        let mut wire = vec![1u8];
657        wire.extend_from_slice(&1000i32.to_be_bytes());
658        wire.extend_from_slice(&breakpoint_event(5, 0xabc));
659        assert!(parse_event_packet(&wire).is_err(), "1000 claimed, 1 supplied");
660    }
661}