use std::collections::{HashMap, HashSet};
pub const STRING_VALUES_CAP: usize = 500_000;
pub struct StringCapture {
inner: HashMap<u32, (u64, u8)>,
cap: usize,
pub truncated: bool,
}
impl StringCapture {
pub fn new(cap: usize) -> Self {
Self {
inner: HashMap::new(),
cap,
truncated: false,
}
}
pub fn insert(&mut self, dense_idx: u32, arr_addr: u64, coder: u8) {
if self.inner.len() >= self.cap && !self.inner.contains_key(&dense_idx) {
self.truncated = true;
return;
}
self.inner.insert(dense_idx, (arr_addr, coder));
}
#[allow(dead_code)]
pub fn is_empty(&self) -> bool {
self.inner.is_empty()
}
#[allow(dead_code)]
pub fn len(&self) -> usize {
self.inner.len()
}
pub fn decode_all<O>(self, open: O, id_size: u8) -> std::io::Result<HashMap<u32, String>>
where
O: Fn() -> std::io::Result<crate::reader::HprofReader>,
{
if self.inner.is_empty() {
return Ok(HashMap::new());
}
let mut arr_coder: HashMap<u64, u8> = HashMap::new();
for &(arr_addr, coder) in self.inner.values() {
arr_coder.entry(arr_addr).or_insert(coder);
}
let wanted: HashSet<u64> = arr_coder.keys().copied().collect();
let mut arr_text: HashMap<u64, String> = HashMap::new();
crate::pass2::scan_prim_arrays(&open, id_size, &wanted, |addr, bytes| {
if let Some(&coder) = arr_coder.get(&addr) {
let s = crate::pass2::decode_java_string(bytes, coder);
arr_text.insert(addr, s);
}
})?;
let mut out = HashMap::with_capacity(self.inner.len());
for (dense_idx, (arr_addr, _coder)) in self.inner {
if let Some(s) = arr_text.get(&arr_addr) {
out.insert(dense_idx, s.clone());
}
}
Ok(out)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn string_capture_below_cap_no_truncation() {
let cap = 10;
let mut sc = StringCapture::new(cap);
for i in 0..cap {
sc.insert(i as u32, i as u64 * 2, 0);
}
assert_eq!(sc.len(), cap, "all {cap} entries must be retained");
assert!(!sc.truncated, "truncated must be false below cap");
}
#[test]
fn string_capture_at_cap_not_truncated() {
let cap = 5;
let mut sc = StringCapture::new(cap);
for i in 0..cap {
sc.insert(i as u32, i as u64, 1);
}
assert_eq!(sc.len(), cap);
assert!(
!sc.truncated,
"truncated must not be true when exactly at cap"
);
}
#[test]
fn string_capture_past_cap_truncated_and_size_bounded() {
let cap = 8;
let mut sc = StringCapture::new(cap);
for i in 0..=(cap as u32) {
sc.insert(i, i as u64, 0);
}
assert!(
sc.truncated,
"truncated must be true after inserting past the cap"
);
assert_eq!(
sc.len(),
cap,
"map must not grow beyond cap; got {}",
sc.len()
);
}
#[test]
fn string_capture_many_past_cap_stays_bounded() {
let cap = 4;
let mut sc = StringCapture::new(cap);
for i in 0..(cap * 3) {
sc.insert(i as u32, i as u64, 0);
}
assert!(sc.truncated, "truncated after many insertions");
assert!(
sc.len() <= cap,
"len {} must not exceed cap {}",
sc.len(),
cap
);
}
#[test]
fn string_capture_update_existing_key_does_not_count_twice() {
let cap = 3;
let mut sc = StringCapture::new(cap);
sc.insert(0, 100, 0);
sc.insert(1, 200, 0);
sc.insert(0, 999, 1);
assert!(
!sc.truncated,
"overwriting an existing key must not trigger truncation"
);
assert_eq!(sc.len(), 2, "still only 2 distinct keys");
}
#[test]
fn string_capture_empty_state() {
let sc = StringCapture::new(STRING_VALUES_CAP);
assert!(sc.is_empty());
assert!(!sc.truncated);
assert_eq!(sc.len(), 0);
}
#[test]
fn string_values_cap_constant_is_500k() {
assert_eq!(STRING_VALUES_CAP, 500_000);
}
}