use jdwp_client::types::{Value, ValueData};
use jdwp_client::JdwpConnection;
use std::collections::{HashMap, HashSet};
const TAG_STRING: u8 = 115;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum FirstRead {
StringContents(u64),
ReferenceType(u64),
}
#[derive(Debug, Default)]
pub struct ValueReads {
strings: HashMap<u64, Option<String>>,
types: HashMap<u64, Option<u64>>,
boxed: HashMap<u64, Option<Value>>,
}
impl ValueReads {
pub fn none() -> Self {
Self::default()
}
pub async fn committed(conn: &JdwpConnection, values: &[&Value]) -> Self {
let mut reads = Self::none();
reads.extend_committed(conn, values).await;
reads
}
pub async fn extend_committed(&mut self, conn: &JdwpConnection, values: &[&Value]) {
let mut seen: HashSet<u64> = HashSet::new();
let mut string_ids: Vec<u64> = Vec::new();
let mut object_ids: Vec<u64> = Vec::new();
for value in values {
match Self::first_read(value) {
Some(FirstRead::StringContents(id)) if !self.strings.contains_key(&id) && seen.insert(id) => {
string_ids.push(id);
}
Some(FirstRead::ReferenceType(id)) if !self.types.contains_key(&id) && seen.insert(id) => {
object_ids.push(id);
}
_ => {}
}
}
let reads = &mut *self;
if !string_ids.is_empty() {
let got = conn.read_string_values_independently(&string_ids).await;
for (id, outcome) in string_ids.iter().zip(got) {
reads.strings.insert(*id, outcome.ok());
}
}
if !object_ids.is_empty() {
let got = conn.read_reference_types_independently(&object_ids).await;
for (id, outcome) in object_ids.iter().zip(got) {
reads.types.insert(*id, outcome.ok());
}
}
}
pub async fn string_contents(&self, conn: &mut JdwpConnection, id: u64) -> Option<String> {
if let Some(prefetched) = self.strings.get(&id) {
return prefetched.clone();
}
conn.get_string_value(id).await.ok()
}
pub async fn committed_boxed(&mut self, conn: &JdwpConnection, reads: &[(u64, Vec<u64>)]) {
if reads.is_empty() {
return;
}
let got = conn.read_object_values_independently(reads).await;
for ((id, _), outcome) in reads.iter().zip(got) {
self.boxed.insert(*id, outcome.ok().and_then(|values| values.into_iter().next()));
}
}
pub fn known_type(&self, id: u64) -> Option<u64> {
self.types.get(&id).copied().flatten()
}
pub async fn boxed_value(&self, conn: &mut JdwpConnection, id: u64, field_id: u64) -> Option<Value> {
if let Some(prefetched) = self.boxed.get(&id) {
return prefetched.clone();
}
conn.get_object_values(id, vec![field_id]).await.ok()?.into_iter().next()
}
pub async fn reference_type(&self, conn: &mut JdwpConnection, id: u64) -> Option<u64> {
if let Some(prefetched) = self.types.get(&id) {
return *prefetched;
}
conn.get_object_reference_type(id).await.ok()
}
const fn first_read(value: &Value) -> Option<FirstRead> {
let ValueData::Object(id) = value.data else { return None };
if id == 0 {
return None;
}
if value.tag == TAG_STRING {
Some(FirstRead::StringContents(id))
} else {
Some(FirstRead::ReferenceType(id))
}
}
}
#[cfg(test)]
mod tests {
use super::{FirstRead, ValueReads, TAG_STRING};
use jdwp_client::types::{Value, ValueData};
fn object(tag: u8, id: u64) -> Value {
Value { tag, data: ValueData::Object(id) }
}
#[test]
fn the_planned_read_follows_the_tag_and_nothing_else() {
assert_eq!(ValueReads::first_read(&object(TAG_STRING, 0x11)), Some(FirstRead::StringContents(0x11)));
assert_eq!(ValueReads::first_read(&object(76, 0x22)), Some(FirstRead::ReferenceType(0x22)));
assert_eq!(ValueReads::first_read(&object(91, 0x33)), Some(FirstRead::ReferenceType(0x33)));
}
#[test]
fn a_value_that_reads_nothing_plans_nothing() {
assert_eq!(ValueReads::first_read(&object(76, 0)), None, "a null object reference reads nothing");
assert_eq!(
ValueReads::first_read(&object(TAG_STRING, 0)),
None,
"a null String reference reads nothing either — the tag does not make it readable"
);
for data in [
ValueData::Int(7),
ValueData::Long(7),
ValueData::Boolean(true),
ValueData::Char(65),
ValueData::Double(1.5),
ValueData::Void,
] {
assert_eq!(
ValueReads::first_read(&Value { tag: 73, data: data.clone() }),
None,
"a primitive renders from the wire and must not be committed: {data:?}"
);
}
}
}