hdf5-pure-rust 0.3.6

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

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 stats_ref(&self) -> &PageBufferStats {
        &self.stats
    }

    pub fn entries(&self) -> btree_map::Values<'_, u64, PageEntry> {
        self.entries.values()
    }

    pub fn entry_ref(&self, addr: u64) -> Option<&PageEntry> {
        self.entries.get(&addr)
    }

    pub fn entry_data(&self, addr: u64) -> Option<&[u8]> {
        self.entry_ref(addr).map(|entry| entry.data.as_slice())
    }

    pub fn print_stats_fmt<W>(&self, out: &mut W) -> fmt::Result
    where
        W: fmt::Write + ?Sized,
    {
        write!(
            out,
            "PageBufferStats(accesses={}, hits={}, misses={}, evictions={}, writes={})",
            self.stats.accesses,
            self.stats.hits,
            self.stats.misses,
            self.stats.evictions,
            self.stats.writes
        )
    }

    pub fn print_stats_into(&self, out: &mut String) {
        out.clear();
        self.print_stats_fmt(out)
            .expect("writing to String cannot fail");
    }

    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 add_new_page_from_slice(&mut self, addr: u64, data: &[u8]) -> Result<()> {
        if data.len() > self.page_size {
            return Err(Error::InvalidFormat(
                "page buffer entry exceeds page size".into(),
            ));
        }
        let owned = Self::copy_page_data(data)?;
        self.insert_entry(PageEntry {
            addr,
            data: owned,
            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 update_entry_from_slice(&mut self, addr: u64, data: &[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"))
        })?;
        Self::write_entry_from_slice(entry, data)?;
        self.stats.writes = self.stats.writes.saturating_add(1);
        Ok(())
    }

    pub fn update_entry_from_slice_ref(&mut self, addr: u64, data: &[u8]) -> Result<&PageEntry> {
        self.update_entry_from_slice(addr, data)?;
        self.entry_ref(addr)
            .ok_or_else(|| Error::InvalidFormat(format!("page buffer entry {addr:#x} not found")))
    }

    pub fn update_entry_from_slice_view(&mut self, addr: u64, data: &[u8]) -> Result<&[u8]> {
        Ok(self
            .update_entry_from_slice_ref(addr, data)?
            .data
            .as_slice())
    }

    pub fn replace_entry_data_from_slice(entry: &mut PageEntry, data: &[u8]) -> Result<()> {
        let next = Self::copy_page_data(data)?;
        entry.data.clear();
        entry.data.extend_from_slice(&next);
        entry.dirty = true;
        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_view(&mut self, addr: u64) -> Option<&[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.as_slice())
        } else {
            self.stats.misses = self.stats.misses.saturating_add(1);
            None
        }
    }

    pub fn read_into(&mut self, addr: u64, out: &mut [u8]) -> Result<bool> {
        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);
            if out.len() != entry.data.len() {
                return Err(Error::InvalidFormat(
                    "page buffer output length mismatch".into(),
                ));
            }
            out.copy_from_slice(&entry.data);
            Ok(true)
        } else {
            self.stats.misses = self.stats.misses.saturating_add(1);
            Ok(false)
        }
    }

    pub fn write(&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(),
            ));
        }
        if let Some(entry) = self.entries.get_mut(&addr) {
            Self::write_entry(entry, data);
            self.stats.writes = self.stats.writes.saturating_add(1);
            return Ok(());
        }
        self.insert_entry(PageEntry {
            addr,
            data,
            dirty: true,
        })?;
        self.stats.writes = self.stats.writes.saturating_add(1);
        Ok(())
    }

    pub fn write_slice(&mut self, addr: u64, data: &[u8]) -> Result<()> {
        if data.len() > self.page_size {
            return Err(Error::InvalidFormat(
                "page buffer entry exceeds page size".into(),
            ));
        }
        if let Some(entry) = self.entries.get_mut(&addr) {
            Self::write_entry_from_slice(entry, data)?;
            self.stats.writes = self.stats.writes.saturating_add(1);
            return Ok(());
        }
        let owned = Self::copy_page_data(data)?;
        self.insert_entry(PageEntry {
            addr,
            data: owned,
            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;
    }

    pub fn write_entry_from_slice(entry: &mut PageEntry, data: &[u8]) -> Result<()> {
        Self::replace_entry_data_from_slice(entry, data)
    }

    fn copy_page_data(data: &[u8]) -> Result<Vec<u8>> {
        let mut owned = Vec::new();
        owned
            .try_reserve_exact(data.len())
            .map_err(|err| Error::InvalidFormat(format!("page buffer allocation failed: {err}")))?;
        owned.extend_from_slice(data);
        Ok(owned)
    }
}

#[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_view(0).unwrap(), b"abc");
        assert!(pb.read_view(8).is_none());
        pb.add_new_page(8, b"def".to_vec()).unwrap();
        assert!(pb.read_view(0).is_none());
        let stats = pb.stats_ref();
        assert_eq!(stats.hits, 1);
        assert_eq!(stats.misses, 2);
        assert_eq!(stats.evictions, 1);

        let mut formatted = String::new();
        pb.print_stats_into(&mut formatted);
        assert!(formatted.contains("hits=1"));
    }

    #[test]
    fn page_buffer_supports_borrowed_and_caller_buffer_reads() {
        let mut pb = PageBuffer::create(8, 2).unwrap();
        pb.write_slice(0, b"abc").unwrap();
        assert_eq!(pb.read_view(0).unwrap(), b"abc");

        let mut out = [0; 3];
        assert!(pb.read_into(0, &mut out).unwrap());
        assert_eq!(&out, b"abc");

        pb.update_entry_from_slice(0, b"defg").unwrap();
        assert!(pb.read_into(0, &mut out).is_err());
        assert_eq!(&out, b"abc");
        assert_eq!(pb.read_view(0).unwrap(), b"defg");
        assert_eq!(pb.entry_data(0).unwrap(), b"defg");
        assert_eq!(pb.entries().count(), 1);

        let updated = pb.update_entry_from_slice_view(0, b"xy").unwrap();
        assert_eq!(updated, b"xy");
        assert!(pb.entry_ref(0).unwrap().dirty);

        let mut formatted = String::from("stale");
        pb.print_stats_into(&mut formatted);
        assert!(formatted.starts_with("PageBufferStats("));
        assert!(!formatted.contains("stale"));
    }
}