Skip to main content

jdwp_client/
types.rs

1// JDWP type definitions
2//
3// Common types used across the JDWP protocol
4
5use serde::{Deserialize, Serialize};
6
7// Object IDs are 8 bytes in JDWP
8pub type ObjectId = u64;
9pub type ThreadId = ObjectId;
10pub type ThreadGroupId = ObjectId;
11pub type StringId = ObjectId;
12pub type ClassLoaderId = ObjectId;
13pub type ClassObjectId = ObjectId;
14pub type ArrayId = ObjectId;
15
16pub type ReferenceTypeId = u64;
17pub type ClassId = ReferenceTypeId;
18pub type InterfaceId = ReferenceTypeId;
19pub type ArrayTypeId = ReferenceTypeId;
20
21pub type MethodId = u64;
22pub type FieldId = u64;
23pub type FrameId = u64;
24
25// Location identifies a code position
26#[derive(Debug, Clone, Serialize, Deserialize)]
27pub struct Location {
28    pub type_tag: u8, // 1=class, 2=interface, 3=array
29    pub class_id: ReferenceTypeId,
30    pub method_id: MethodId,
31    pub index: u64, // bytecode index (PC)
32}
33
34// Thread status values
35#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
36#[repr(u32)]
37pub enum ThreadStatus {
38    Zombie = 0,
39    Running = 1,
40    Sleeping = 2,
41    Monitor = 3,
42    Wait = 4,
43}
44
45// Suspend status values
46#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
47#[repr(u32)]
48pub enum SuspendStatus {
49    Running = 0,
50    Suspended = 1,
51}
52
53// Type tags for values
54#[derive(Debug, Clone, Copy, PartialEq, Eq)]
55#[repr(u8)]
56pub enum TypeTag {
57    Array = 91,        // '['
58    Byte = 66,         // 'B'
59    Char = 67,         // 'C'
60    Object = 76,       // 'L'
61    Float = 70,        // 'F'
62    Double = 68,       // 'D'
63    Int = 73,          // 'I'
64    Long = 74,         // 'J'
65    Short = 83,        // 'S'
66    Void = 86,         // 'V'
67    Boolean = 90,      // 'Z'
68    String = 115,      // 's'
69    Thread = 116,      // 't'
70    ThreadGroup = 103, // 'g'
71    ClassLoader = 108, // 'l'
72    ClassObject = 99,  // 'c'
73}
74
75// Tagged value
76#[derive(Debug, Clone, Serialize, Deserialize)]
77pub struct Value {
78    pub tag: u8,
79    pub data: ValueData,
80}
81
82#[derive(Debug, Clone, Serialize, Deserialize)]
83#[serde(untagged)]
84pub enum ValueData {
85    Byte(i8),
86    Char(u16),
87    Float(f32),
88    Double(f64),
89    Int(i32),
90    Long(i64),
91    Short(i16),
92    Boolean(bool),
93    Object(ObjectId),
94    Void,
95}
96
97impl ValueData {
98    /// Render a primitive wire value. `None` for a reference, which cannot be described without asking
99    /// the debuggee about it.
100    ///
101    /// **One renderer, one home** (TYPE-1,
102    /// [#48](https://github.com/YgorPerez/java-debugging-mcp/issues/48)). `mcp-server` carried a
103    /// byte-identical copy of this match called `render_primitive`, and the copy was the one the tool
104    /// actually ran — `Value::format` below was reached only through array elements and the
105    /// type-mismatch message. That is what made this file's coverage number a lie: it measured 16.67%
106    /// region and the review's verdict was "most arms are for types the probes never produce", which was
107    /// half the answer. The other half was that the arms were *bypassed*. Rendering a wire value belongs
108    /// to the crate that reads the wire, so the seam is here and `mcp-server` calls across it; the
109    /// `Option` is what keeps the two audiences apart, because `mcp-server` has a much better answer for
110    /// a reference than an id and is the only side that can pay a round trip to get it.
111    #[must_use]
112    pub fn format_primitive(&self) -> Option<String> {
113        Some(match self {
114            Self::Byte(v) => format!("(byte) {v}"),
115            Self::Char(v) => format_char(*v),
116            Self::Float(v) => format!("(float) {v}"),
117            Self::Double(v) => format!("(double) {v}"),
118            Self::Int(v) => format!("(int) {v}"),
119            Self::Long(v) => format!("(long) {v}"),
120            Self::Short(v) => format!("(short) {v}"),
121            Self::Boolean(v) => format!("(boolean) {v}"),
122            Self::Void => "(void)".to_string(),
123            Self::Object(_) => return None,
124        })
125    }
126}
127
128/// Render one Java `char`, which is a UTF-16 **code unit** and not a Unicode scalar value.
129///
130/// The two are different sizes of thing, and half a surrogate pair is a perfectly ordinary value to find
131/// in a `char` field or a `char[]` — a string sliced mid-pair leaves one behind, which is a real bug class
132/// someone would reach for a debugger to chase. `char::from_u32` refuses exactly those code units, and
133/// this used to answer `unwrap_or('?')`: `(char) 0xD800` came back as `(char) '?'`, byte for byte the same
134/// as a genuine question mark, so the debugger hid the very thing it was being asked about (TYPE-1, #48).
135///
136/// Rendered as the `'\uD800'` escape Java itself would print, plus what it is, so the two readings can
137/// never be confused again.
138fn format_char(unit: u16) -> String {
139    // `from_u32` fails only on the surrogate range, so `None` here IS "this is not a character".
140    char::from_u32(u32::from(unit)).map_or_else(
141        || format!("(char) '\\u{unit:04X}' (unpaired surrogate, not a character)"),
142        |c| format!("(char) '{c}'"),
143    )
144}
145
146impl Value {
147    /// Format value for display.
148    ///
149    /// A thin shell over [`ValueData::format_primitive`] plus the one kind it declines: a reference, which
150    /// this crate can only name by its id.
151    #[must_use]
152    pub fn format(&self) -> String {
153        match &self.data {
154            ValueData::Object(0) => "(object) null".to_string(),
155            ValueData::Object(id) => format!("(object) @{id:x}"),
156            primitive => primitive.format_primitive().unwrap_or_default(),
157        }
158    }
159}
160
161// Variable information
162#[derive(Debug, Clone, Serialize, Deserialize)]
163pub struct Variable {
164    pub code_index: u64,
165    pub name: String,
166    pub signature: String,
167    /// The **generic** signature from the class file's `Signature` attribute, when it carries one
168    /// (DISC-12, #95).
169    ///
170    /// `None` is the ordinary answer, not a degraded one: the attribute is optional, absent for code
171    /// compiled without it and for synthetic members whose types were erased. JDWP's generic commands
172    /// answer with an EMPTY STRING in that case rather than an error, and an empty string is normalised to
173    /// `None` here so that no caller can render a blank type.
174    pub generic_signature: Option<String>,
175    pub length: u32,
176    pub slot: u32,
177}
178
179// Stack frame information
180#[derive(Debug, Clone, Serialize, Deserialize)]
181pub struct FrameInfo {
182    pub frame_id: FrameId,
183    pub location: Location,
184}
185
186#[cfg(test)]
187mod tests {
188    use super::{Value, ValueData};
189
190    /// TYPE-1 (#48): `(char) 0xD800` is half a surrogate pair — an ordinary thing to find in a Java
191    /// `char[]`, since a `char` is a UTF-16 code unit and not a Unicode scalar value — and it used to
192    /// render as `(char) '?'`, byte for byte a real question mark.
193    ///
194    /// The comparison against a genuine `'?'` is the whole test. "Renders as something" was never the
195    /// missing property; "renders as something a caller can tell apart from a real value" was.
196    #[test]
197    fn an_unpaired_surrogate_is_rendered_apart_from_a_real_question_mark() {
198        let surrogate = ValueData::Char(0xD800).format_primitive().expect("a char is a primitive");
199        let question = ValueData::Char(u16::from(b'?')).format_primitive().expect("so is this one");
200
201        assert_eq!(question, "(char) '?'", "a real question mark still renders as itself");
202        assert_ne!(surrogate, question, "the two must not be the same bytes: {surrogate}");
203        assert!(surrogate.contains("\\uD800"), "the code unit itself is shown: {surrogate}");
204        assert!(surrogate.contains("unpaired surrogate"), "and what it is: {surrogate}");
205
206        // The range is 0xD800..=0xDFFF, not one code unit, and the low half is what a string sliced
207        // mid-pair actually leaves behind.
208        let low = ValueData::Char(0xDFFF).format_primitive().expect("still a char");
209        assert!(low.contains("\\uDFFF"), "the low half of the range is covered too: {low}");
210        assert!(low.contains("unpaired surrogate"), "{low}");
211
212        // Everything either side of the range is a character and renders as one.
213        assert_eq!(ValueData::Char(0xD7FF).format_primitive().unwrap(), "(char) '\u{d7ff}'");
214        assert_eq!(ValueData::Char(0xE000).format_primitive().unwrap(), "(char) '\u{e000}'");
215    }
216
217    /// One renderer, one home (TYPE-1, #48). `mcp-server` used to carry a byte-identical copy of the
218    /// primitive match and call *that*, so this crate's copy was cold and its coverage number said
219    /// nothing. Both sides now cross the same seam, and the only thing that differs is what each does
220    /// with the reference `format_primitive` declines.
221    #[test]
222    fn only_a_reference_declines_to_render_without_asking_the_debuggee() {
223        assert_eq!(ValueData::Int(-2_147_483_648).format_primitive().unwrap(), "(int) -2147483648");
224        assert_eq!(ValueData::Boolean(true).format_primitive().unwrap(), "(boolean) true");
225        assert_eq!(ValueData::Byte(-7).format_primitive().unwrap(), "(byte) -7");
226        assert_eq!(ValueData::Short(-300).format_primitive().unwrap(), "(short) -300");
227        assert_eq!(ValueData::Long(9_000_000_000).format_primitive().unwrap(), "(long) 9000000000");
228        assert_eq!(ValueData::Float(1.5).format_primitive().unwrap(), "(float) 1.5");
229        assert_eq!(ValueData::Double(-2.25).format_primitive().unwrap(), "(double) -2.25");
230
231        assert!(
232            ValueData::Object(0x2b).format_primitive().is_none(),
233            "a reference needs a round trip to describe, and this crate is not the side that pays it"
234        );
235        // `Value::format` is the shell that answers anyway, with the id it can see from here.
236        assert_eq!(Value { tag: 76, data: ValueData::Object(0x2b) }.format(), "(object) @2b");
237        assert_eq!(Value { tag: 76, data: ValueData::Object(0) }.format(), "(object) null");
238        // …and defers to the same renderer for everything else, so the two can never drift apart again.
239        assert_eq!(Value { tag: 67, data: ValueData::Char(0xD800) }.format(), surrogate_rendering());
240    }
241
242    fn surrogate_rendering() -> String {
243        ValueData::Char(0xD800).format_primitive().expect("a char is a primitive")
244    }
245}