use flate2::read::GzDecoder;
use std::{
fs::File,
io::{self, BufReader, Read},
};
const BUF_CAP: usize = 8 << 20;
pub struct HprofReader {
pub format: String,
pub id_size: u8,
pub timestamp_ms: u64,
inner: Box<dyn Read>,
buf: Vec<u8>,
pos: usize,
end: usize,
}
impl HprofReader {
pub fn open(path: &str) -> io::Result<Self> {
let file = File::open(path)?;
let mut peek = BufReader::new(file);
let mut magic = [0u8; 2];
peek.read_exact(&mut magic)?;
let stream = io::Cursor::new(magic.to_vec()).chain(peek);
let inner: Box<dyn Read> = if magic == [0x1f, 0x8b] {
Box::new(GzDecoder::new(stream))
} else {
Box::new(stream)
};
let mut r = HprofReader {
format: String::new(),
id_size: 4,
timestamp_ms: 0,
inner,
buf: vec![0u8; BUF_CAP],
pos: 0,
end: 0,
};
r.read_header()?;
Ok(r)
}
fn read_header(&mut self) -> io::Result<()> {
let mut s = Vec::new();
loop {
let b = self.u1()?;
if b == 0 {
break;
}
s.push(b);
}
self.format = String::from_utf8_lossy(&s).into_owned();
let id_size = self.u4()?;
if id_size != 4 && id_size != 8 {
return Err(io::Error::new(
io::ErrorKind::InvalidData,
format!("unsupported id_size in HPROF header: {id_size} (expected 4 or 8)"),
));
}
self.id_size = id_size as u8;
self.timestamp_ms = self.u8()?;
Ok(())
}
#[cold]
fn refill(&mut self) -> io::Result<usize> {
let leftover = self.end - self.pos;
if leftover > 0 {
self.buf.copy_within(self.pos..self.end, 0);
}
self.pos = 0;
self.end = leftover;
while self.end < self.buf.len() {
let n = self.inner.read(&mut self.buf[self.end..])?;
if n == 0 {
break;
}
self.end += n;
}
Ok(self.end - self.pos)
}
#[inline]
fn ensure(&mut self, n: usize) -> io::Result<()> {
if self.end - self.pos >= n {
return Ok(());
}
self.refill()?;
if self.end - self.pos >= n {
Ok(())
} else {
Err(io::Error::new(
io::ErrorKind::UnexpectedEof,
"unexpected eof",
))
}
}
#[inline]
pub fn u1(&mut self) -> io::Result<u8> {
if self.pos >= self.end {
self.ensure(1)?;
}
let b = self.buf[self.pos];
self.pos += 1;
Ok(b)
}
#[inline]
pub fn u2(&mut self) -> io::Result<u16> {
self.ensure(2)?;
let p = self.pos;
let v = u16::from_be_bytes([self.buf[p], self.buf[p + 1]]);
self.pos = p + 2;
Ok(v)
}
#[inline]
pub fn u4(&mut self) -> io::Result<u32> {
self.ensure(4)?;
let p = self.pos;
let v = u32::from_be_bytes([
self.buf[p],
self.buf[p + 1],
self.buf[p + 2],
self.buf[p + 3],
]);
self.pos = p + 4;
Ok(v)
}
#[inline]
pub fn u8(&mut self) -> io::Result<u64> {
self.ensure(8)?;
let p = self.pos;
let v = u64::from_be_bytes([
self.buf[p],
self.buf[p + 1],
self.buf[p + 2],
self.buf[p + 3],
self.buf[p + 4],
self.buf[p + 5],
self.buf[p + 6],
self.buf[p + 7],
]);
self.pos = p + 8;
Ok(v)
}
#[inline]
pub fn id(&mut self) -> io::Result<u64> {
match self.id_size {
4 => Ok(self.u4()? as u64),
8 => self.u8(),
s => Err(io::Error::new(
io::ErrorKind::InvalidData,
format!("unsupported id_size: {s}"),
)),
}
}
pub fn skip(&mut self, mut n: u64) -> io::Result<()> {
while n > 0 {
let avail = self.end - self.pos;
if avail == 0 {
if self.refill()? == 0 {
return Err(io::Error::new(io::ErrorKind::UnexpectedEof, "eof in skip"));
}
continue;
}
let take = (avail as u64).min(n) as usize;
self.pos += take;
n -= take as u64;
}
Ok(())
}
pub fn read_bytes(&mut self, n: usize) -> io::Result<Vec<u8>> {
let mut v = vec![0u8; n];
self.read_into(&mut v)?;
Ok(v)
}
pub fn read_bytes_reuse(&mut self, buf: &mut Vec<u8>, n: usize) -> io::Result<()> {
buf.resize(n, 0);
self.read_into(buf)
}
fn read_into(&mut self, dst: &mut [u8]) -> io::Result<()> {
let mut written = 0usize;
while written < dst.len() {
let avail = self.end - self.pos;
if avail > 0 {
let take = avail.min(dst.len() - written);
dst[written..written + take].copy_from_slice(&self.buf[self.pos..self.pos + take]);
self.pos += take;
written += take;
} else {
let remaining = dst.len() - written;
if remaining >= BUF_CAP {
self.inner.read_exact(&mut dst[written..])?;
written = dst.len();
} else if self.refill()? == 0 {
return Err(io::Error::new(
io::ErrorKind::UnexpectedEof,
"eof in read_into",
));
}
}
}
Ok(())
}
}
#[cfg(test)]
mod tests {
use super::*;
fn dump_plain() -> Option<String> {
std::env::var("HPROF_TEST_DUMP").ok()
}
fn dump_gz() -> Option<String> {
std::env::var("HPROF_TEST_DUMP_GZ").ok()
}
#[test]
fn read_header_plain() {
let Some(dump) = dump_plain() else {
return;
};
let r = HprofReader::open(&dump).unwrap();
assert!(
r.id_size == 4 || r.id_size == 8,
"bad id_size {}",
r.id_size
);
assert!(
r.format.starts_with("JAVA PROFILE"),
"bad format {:?}",
r.format
);
assert!(r.timestamp_ms > 0, "timestamp should be nonzero");
}
#[test]
fn read_header_gz() {
let Some(dump) = dump_gz() else {
return;
};
let r = HprofReader::open(&dump).unwrap();
assert!(r.id_size == 4 || r.id_size == 8);
assert!(r.format.starts_with("JAVA PROFILE"));
}
#[test]
fn read_primitives() {
let data: Vec<u8> = vec![
0xAB, 0x12, 0x34, 0xDE, 0xAD, 0xBE, 0xEF, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07,
0x08,
];
let mut r = HprofReader {
format: String::new(),
id_size: 4,
timestamp_ms: 0,
inner: Box::new(io::Cursor::new(data)),
buf: vec![0u8; BUF_CAP],
pos: 0,
end: 0,
};
assert_eq!(r.u1().unwrap(), 0xAB);
assert_eq!(r.u2().unwrap(), 0x1234);
assert_eq!(r.u4().unwrap(), 0xDEADBEEF);
assert_eq!(r.u8().unwrap(), 0x0102030405060708);
}
#[test]
fn skip_and_read() {
let data: Vec<u8> = (0..100u8).collect();
let mut r = HprofReader {
format: String::new(),
id_size: 8,
timestamp_ms: 0,
inner: Box::new(io::Cursor::new(data)),
buf: vec![0u8; BUF_CAP],
pos: 0,
end: 0,
};
assert_eq!(r.u1().unwrap(), 0);
r.skip(9).unwrap(); assert_eq!(r.u1().unwrap(), 10);
let mut buf = Vec::new();
r.read_bytes_reuse(&mut buf, 5).unwrap();
assert_eq!(buf, vec![11, 12, 13, 14, 15]);
}
fn header_blob(id_size: u32) -> Vec<u8> {
let mut v = b"JAVA PROFILE 1.0.2\0".to_vec();
v.extend_from_slice(&id_size.to_be_bytes());
v.extend_from_slice(&1u64.to_be_bytes());
v
}
fn reader_over(data: Vec<u8>) -> HprofReader {
HprofReader {
format: String::new(),
id_size: 0,
timestamp_ms: 0,
inner: Box::new(io::Cursor::new(data)),
buf: vec![0u8; BUF_CAP],
pos: 0,
end: 0,
}
}
#[test]
fn read_header_accepts_4_and_8() {
for sz in [4u32, 8] {
let mut r = reader_over(header_blob(sz));
r.read_header().unwrap();
assert_eq!(r.id_size, sz as u8);
assert!(r.format.starts_with("JAVA PROFILE"));
}
}
#[test]
fn read_header_rejects_bad_id_size() {
for sz in [0u32, 2, 16, 260] {
let mut r = reader_over(header_blob(sz));
let err = r.read_header().unwrap_err();
assert_eq!(err.kind(), io::ErrorKind::InvalidData, "sz={sz}");
}
}
}