AkurAI Build
Menu

Public repository / master

AkurAI-Framework / crates/blobs/src/lib.rs

Latest change 796c2f22cc110ffe57e2bb53ebf4ddc47f4ca77e - Add multipart/form-data parsing and content-addressed blob store by Ólafur Búi Ólafsson

//! AkurAI-Framework content-addressed blob store — pure `std`, zero external
//! dependencies (it rests on the framework's own [`akurai_storage`] B+tree).
//!
//! A [`BlobStore`] keeps arbitrary byte payloads keyed by a hash of their own
//! content. Content addressing buys two things for free:
//!
//! * **Deduplication** — identical bytes hash to the same id, so storing the
//!   same file twice is a no-op and costs no extra space.
//! * **Integrity of reference** — the id *is* a digest of the data, so a caller
//!   holding an id is naming an exact byte sequence, not a mutable slot.
//!
//! ## The hash
//!
//! The id is a 128-bit [FNV-1a] digest rendered as 32 lowercase hex chars.
//! FNV-1a is **not cryptographic** — it is fast, std-only, and deterministic,
//! which is all a content key for trusted local uploads needs. It is *not* a
//! defense against an adversary deliberately crafting two different inputs that
//! collide. Widening the digest to 128 bits (versus the 64-bit hash the asset
//! fingerprinter uses) makes accidental collisions astronomically unlikely for
//! the upload volumes this store is built for. If a future requirement needs
//! collision resistance against malicious input, swap [`content_id`] for a
//! cryptographic hash; nothing else in the API changes.
//!
//! [FNV-1a]: https://en.wikipedia.org/wiki/Fowler–Noll–Vo_hash_function
//! [`akurai_storage`]: akurai_storage

#![forbid(unsafe_code)]

use std::io;
use std::path::Path;

use akurai_storage::BTree;

/// FNV-1a 128-bit offset basis and prime (see the module docs for the caveat).
const FNV128_OFFSET_BASIS: u128 = 0x6c62_272e_07bb_0142_62b8_2175_6295_c58d;
const FNV128_PRIME: u128 = 0x0000_0000_0100_0000_0000_0000_0000_013b;

/// A persistent, content-addressed store of byte payloads.
///
/// Each blob is keyed by [`content_id`] of its bytes and stored verbatim, so a
/// `put` followed by `get` round-trips the exact payload. Backed by a single
/// [`BTree`] file; every mutating call commits, so writes survive a reopen.
pub struct BlobStore {
    tree: BTree,
}

impl BlobStore {
    /// Open (creating if absent) the blob store at `path`.
    pub fn open(path: impl AsRef<Path>) -> io::Result<BlobStore> {
        Ok(BlobStore {
            tree: BTree::open(path)?,
        })
    }

    /// Store `bytes`, returning their content id (32-char lowercase hex).
    ///
    /// Idempotent: storing bytes already present writes nothing and returns the
    /// same id, so identical payloads are deduplicated automatically.
    pub fn put(&mut self, bytes: &[u8]) -> io::Result<String> {
        let id = content_id(bytes);
        if self.tree.get(id.as_bytes())?.is_none() {
            self.tree.insert(id.as_bytes(), bytes)?;
            self.tree.commit()?;
        }
        Ok(id)
    }

    /// Fetch the bytes for `id`, or `None` if no such blob exists.
    pub fn get(&mut self, id: &str) -> io::Result<Option<Vec<u8>>> {
        self.tree.get(id.as_bytes())
    }

    /// Whether a blob with `id` is present.
    pub fn exists(&mut self, id: &str) -> io::Result<bool> {
        Ok(self.tree.get(id.as_bytes())?.is_some())
    }

    /// Remove the blob `id`. Returns whether it was present. Committed on
    /// success so the deletion survives a reopen.
    pub fn delete(&mut self, id: &str) -> io::Result<bool> {
        let removed = self.tree.delete(id.as_bytes())?;
        if removed {
            self.tree.commit()?;
        }
        Ok(removed)
    }
}

/// Content id of `bytes`: a 128-bit FNV-1a digest as 32 lowercase hex chars.
///
/// Deterministic — identical bytes always yield the same id, and a single
/// flipped byte reliably changes it. Not cryptographic (see the module docs).
pub fn content_id(bytes: &[u8]) -> String {
    let mut hash = FNV128_OFFSET_BASIS;
    for &b in bytes {
        hash ^= b as u128;
        hash = hash.wrapping_mul(FNV128_PRIME);
    }
    format!("{hash:032x}")
}

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

    // Filesystem-backed `BlobStore` tests live in `tests/store.rs`, where
    // `CARGO_TARGET_TMPDIR` is defined (it is only set for integration tests).
    // These unit tests cover the pure hashing logic.

    #[test]
    fn content_id_is_stable_and_sensitive() {
        assert_eq!(content_id(b"x"), content_id(b"x"));
        assert_ne!(content_id(b"x"), content_id(b"y"));
        assert_eq!(content_id(b"").len(), 32);
        // Single flipped byte changes the digest.
        assert_ne!(content_id(b"aaaa"), content_id(b"aaab"));
    }
}