ipfs-embed-db 0.6.0

small embeddable ipfs implementation
Documentation
use crate::metadata::Metadata;
use anyhow::Result;
use libipld::cid::Cid;
use std::collections::{HashMap, HashSet};
use thiserror::Error;

#[derive(Debug, Error)]
pub enum InvalidTransaction {
    #[error("block not found")]
    BlockNotFound,
    #[error("unpin underflow: {0}")]
    UnpinUnderflow(u64),
}

pub struct StateBuilder {
    metadata: Metadata,
    next_id: u64,
    lookup: HashMap<Cid, u64>,
    pins: HashMap<u64, u64>,
    rrefs: HashSet<(u64, u64)>,
}

impl StateBuilder {
    pub fn new(metadata: Metadata, next_id: u64) -> Self {
        Self {
            metadata,
            next_id,
            lookup: Default::default(),
            pins: Default::default(),
            rrefs: Default::default(),
        }
    }

    fn _get(&mut self, cid: &Cid) -> Result<Option<u64>> {
        let id = if let Some(id) = self.lookup.get(&cid) {
            Some(*id)
        } else {
            let id = self.metadata.id(cid)?;
            if let Some(id) = id {
                self.lookup.insert(cid.clone(), id);
            }
            id
        };
        Ok(id)
    }

    pub fn get(&mut self, cid: &Cid) -> Result<u64> {
        Ok(self._get(cid)?.ok_or(InvalidTransaction::BlockNotFound)?)
    }

    pub fn insert(&mut self, cid: &Cid) -> Result<u64> {
        let id = if let Some(id) = self._get(cid)? {
            id
        } else {
            let id = self.next_id;
            self.next_id += 1;
            self.lookup.insert(cid.clone(), id);
            id
        };
        Ok(id)
    }

    pub fn pin(&mut self, cid: &Cid) -> Result<()> {
        let id = self.get(cid)?;
        let pins = if let Some(pins) = self.pins.get(&id) {
            *pins
        } else {
            self.metadata.pin(id)?.unwrap_or_default()
        };
        self.pins.insert(id, pins + 1);
        Ok(())
    }

    pub fn unpin(&mut self, cid: &Cid) -> Result<()> {
        let id = self.get(cid)?;
        let pins = if let Some(pins) = self.pins.get(&id) {
            *pins
        } else {
            self.metadata
                .pin(id)?
                .ok_or(InvalidTransaction::BlockNotFound)?
        };
        if pins < 1 {
            return Err(InvalidTransaction::UnpinUnderflow(id).into());
        }
        self.pins.insert(id, pins - 1);
        Ok(())
    }

    pub fn pins<'a>(&'a self) -> impl Iterator<Item = (u64, u64)> + 'a {
        self.pins.iter().map(|(k, v)| (*k, *v))
    }

    pub fn remove_reference(&mut self, edge: (u64, u64)) {
        self.rrefs.insert(edge);
    }

    pub fn is_live(&self, id: u64) -> Result<bool> {
        if let Some(pin) = self.pins.get(&id) {
            if *pin != 0 {
                return Ok(true);
            }
        } else if self.metadata.pin(id)?.unwrap_or_default() != 0 {
            return Ok(true);
        }
        log::trace!("not pinned: {}", id);

        for id2 in self.metadata.referrers(id) {
            let id2 = id2?;
            if !self.rrefs.contains(&(id2, id)) {
                log::trace!("{} referenced by {}", id, id2);
                return Ok(true);
            }
        }
        Ok(false)
    }

    pub fn begin(self) -> u64 {
        self.next_id
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use libipld::cid::RAW;
    use libipld::multihash::{Multihash, MultihashDigest, SHA2_256};
    use tempdir::TempDir;

    fn setup() -> (TempDir, Metadata) {
        let tmp = TempDir::new("state").unwrap();
        let db = sled::open(tmp.path()).unwrap();
        let tree = db.open_tree("metadata").unwrap();
        let metadata = Metadata::new(tree);
        (tmp, metadata)
    }

    fn cid(id: u64) -> Cid {
        let mh = Multihash::new(SHA2_256, &id.to_be_bytes()[..]).unwrap();
        Cid::new_v1(RAW, mh.to_raw().unwrap())
    }

    #[test]
    fn test_state() {
        let (_tmp, metadata) = setup();

        let cid = [cid(0), cid(1)];

        metadata.insert(&cid[0], 0).unwrap();
        metadata.set_pin(0, 3).unwrap();

        let mut state = StateBuilder::new(metadata.clone(), 1);
        assert_eq!(state.get(&cid[0]).unwrap(), 0);
        assert_eq!(state.insert(&cid[0]).unwrap(), 0);
        assert_eq!(state.insert(&cid[1]).unwrap(), 1);
        assert_eq!(state.get(&cid[1]).unwrap(), 1);

        state.pin(&cid[0]).unwrap();
        state.pin(&cid[0]).unwrap();
        state.unpin(&cid[0]).unwrap();

        state.pin(&cid[1]).unwrap();
        state.pin(&cid[1]).unwrap();
        state.unpin(&cid[1]).unwrap();

        assert_eq!(state.pins.len(), 2);
        assert_eq!(state.pins.get(&0), Some(&4));
        assert_eq!(state.pins.get(&1), Some(&1));
        assert_eq!(state.begin(), 2);
    }
}