hdf5-pure-rust 0.3.2

Pure Rust implementation of the HDF5 file format
Documentation
use std::collections::BTreeMap;

use crate::error::{Error, Result};

#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub struct PageBufferStats {
    pub accesses: u64,
    pub hits: u64,
    pub misses: u64,
    pub evictions: u64,
    pub writes: u64,
}

#[derive(Debug, Clone, PartialEq, Eq)]
pub struct PageEntry {
    pub addr: u64,
    pub data: Vec<u8>,
    pub dirty: bool,
}

#[derive(Debug, Clone, PartialEq, Eq)]
pub struct PageBuffer {
    page_size: usize,
    capacity_pages: usize,
    entries: BTreeMap<u64, PageEntry>,
    stats: PageBufferStats,
    enabled: bool,
}

impl PageBuffer {
    pub fn create(page_size: usize, capacity_pages: usize) -> Result<Self> {
        if page_size == 0 {
            return Err(Error::InvalidFormat("page buffer page size is zero".into()));
        }
        Ok(Self {
            page_size,
            capacity_pages,
            entries: BTreeMap::new(),
            stats: PageBufferStats::default(),
            enabled: capacity_pages > 0,
        })
    }

    pub fn reset_stats(&mut self) {
        self.stats = PageBufferStats::default();
    }

    pub fn get_stats(&self) -> PageBufferStats {
        self.stats.clone()
    }

    pub fn print_stats(&self) -> String {
        format!(
            "PageBufferStats(accesses={}, hits={}, misses={}, evictions={}, writes={})",
            self.stats.accesses,
            self.stats.hits,
            self.stats.misses,
            self.stats.evictions,
            self.stats.writes
        )
    }

    pub fn flush_cb(entry: &mut PageEntry) {
        entry.dirty = false;
    }

    pub fn flush(&mut self) {
        for entry in self.entries.values_mut() {
            Self::flush_cb(entry);
        }
    }

    pub fn dest_cb(_entry: PageEntry) {}

    pub fn dest(mut self) {
        self.entries.clear();
        self.enabled = false;
    }

    pub fn add_new_page(&mut self, addr: u64, data: Vec<u8>) -> Result<()> {
        if data.len() > self.page_size {
            return Err(Error::InvalidFormat(
                "page buffer entry exceeds page size".into(),
            ));
        }
        self.insert_entry(PageEntry {
            addr,
            data,
            dirty: false,
        })
    }

    pub fn update_entry(&mut self, addr: u64, data: Vec<u8>) -> Result<()> {
        if data.len() > self.page_size {
            return Err(Error::InvalidFormat(
                "page buffer entry exceeds page size".into(),
            ));
        }
        let entry = self.entries.get_mut(&addr).ok_or_else(|| {
            Error::InvalidFormat(format!("page buffer entry {addr:#x} not found"))
        })?;
        entry.data = data;
        entry.dirty = true;
        self.stats.writes = self.stats.writes.saturating_add(1);
        Ok(())
    }

    pub fn remove_entry(&mut self, addr: u64) -> Result<PageEntry> {
        self.entries
            .remove(&addr)
            .ok_or_else(|| Error::InvalidFormat(format!("page buffer entry {addr:#x} not found")))
    }

    pub fn read(&mut self, addr: u64) -> Option<Vec<u8>> {
        self.stats.accesses = self.stats.accesses.saturating_add(1);
        if let Some(entry) = self.entries.get(&addr) {
            self.stats.hits = self.stats.hits.saturating_add(1);
            Some(entry.data.clone())
        } else {
            self.stats.misses = self.stats.misses.saturating_add(1);
            None
        }
    }

    pub fn write(&mut self, addr: u64, data: Vec<u8>) -> Result<()> {
        if self.entries.contains_key(&addr) {
            self.update_entry(addr, data)
        } else {
            self.insert_entry(PageEntry {
                addr,
                data,
                dirty: true,
            })?;
            self.stats.writes = self.stats.writes.saturating_add(1);
            Ok(())
        }
    }

    pub fn enabled(&self) -> bool {
        self.enabled
    }

    pub fn insert_entry(&mut self, entry: PageEntry) -> Result<()> {
        if entry.data.len() > self.page_size {
            return Err(Error::InvalidFormat(
                "page buffer entry exceeds page size".into(),
            ));
        }
        if self.capacity_pages == 0 {
            return Ok(());
        }
        while self.entries.len() >= self.capacity_pages {
            let Some((&addr, _)) = self.entries.iter().next() else {
                break;
            };
            self.entries.remove(&addr);
            self.stats.evictions = self.stats.evictions.saturating_add(1);
        }
        self.entries.insert(entry.addr, entry);
        Ok(())
    }

    pub fn write_entry(entry: &mut PageEntry, data: Vec<u8>) {
        entry.data = data;
        entry.dirty = true;
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn page_buffer_tracks_hits_misses_and_evictions() {
        let mut pb = PageBuffer::create(8, 1).unwrap();
        pb.add_new_page(0, b"abc".to_vec()).unwrap();
        assert_eq!(pb.read(0).unwrap(), b"abc");
        assert!(pb.read(8).is_none());
        pb.add_new_page(8, b"def".to_vec()).unwrap();
        assert!(pb.read(0).is_none());
        let stats = pb.get_stats();
        assert_eq!(stats.hits, 1);
        assert_eq!(stats.misses, 2);
        assert_eq!(stats.evictions, 1);
    }
}