//! The record block shape (`MSR1` + bincode), kept only so the ipfs tier can //! recognise — and scrub — record blocks that older builds wrote into the block //! table (`filestore::scrub_record_blocks`). Copied from meta-share's //! `store.rs`; the record *cache* stays in the hull. use std::collections::BTreeMap; use serde::{Deserialize, Serialize}; /// One metadata record. `cid` is the externally-canonical CID — preferred /// for content addressing across the swarm, derived by rank (IPFS > /// sha-family > btih > midhash) from the source peer's bare-CID key-set — /// `canonical_cid` is no longer a stored field (see ingest::cid_rank). /// `cids` carries every CID variant the source peer knows about for this /// file, so a lookup by any of them (sha256 when canonical is ipfs, etc.) /// can still match locally. /// /// `tokens` is retained on the wire/struct shape for backward compatibility /// with peers and blocks produced by older builds (it serialises empty now /// that there is no tokenizer); decoders tolerate either. /// /// `fields` carries human-readable key/value metadata (fileName, sizeByte, /// mimeType, etc.) that the UI renders as a table. /// /// `#[serde(default)]` on `cids` and `fields` keeps the wire format /// backward-compatible with peers running older builds — they emit empty /// values and we tolerate them on receive. #[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] pub struct Record { pub cid: String, pub title: String, pub year: Option, pub tokens: Vec, #[serde(default)] pub cids: Vec, #[serde(default)] pub fields: BTreeMap, } /// Encoding format errors for record blocks stored in `RedbBlockstore`. /// Kept narrow so callers can distinguish "wrong format on disk" from /// "transport / IO failure" without string-matching. #[derive(Debug, thiserror::Error)] pub enum RecordBlockError { #[error("block too short to be an MSR1 record (got {0} bytes)")] TooShort(usize), #[error("block magic mismatch: expected MSR1, got {0:?}")] BadMagic([u8; 4]), #[error("bincode encode failed: {0}")] Encode(#[from] bincode::error::EncodeError), #[error("bincode decode failed: {0}")] Decode(#[from] bincode::error::DecodeError), } impl Record { /// Serialise to the on-disk block form used by `RedbBlockstore`: /// `MSR1` magic prefix + bincode-encoded payload. The magic gives /// us a single-byte cheap reject on format drift; future record- /// shape changes bump to `MSR2` and decoders refuse the old shape /// loudly rather than silently producing garbage. /// /// Test-only: nothing writes record blocks any more. Tests build them to /// exercise the boot scrub and the relay sniff that still recognise them. #[cfg(test)] pub fn encode_block(&self) -> Result, RecordBlockError> { let payload = bincode::serde::encode_to_vec(self, bincode::config::standard())?; let mut out = Vec::with_capacity(crate::blockstore::RECORD_BLOCK_MAGIC.len() + payload.len()); out.extend_from_slice(crate::blockstore::RECORD_BLOCK_MAGIC); out.extend_from_slice(&payload); Ok(out) } /// Decode an MSR1-prefixed block back into a `Record`. Rejects /// anything that doesn't start with the magic — including blocks /// produced by the IPFS gateway tier (raw bytes / dag-pb), which /// share the same blockstore but never carry our prefix. pub fn decode_block(bytes: &[u8]) -> Result { let magic = crate::blockstore::RECORD_BLOCK_MAGIC; if bytes.len() < magic.len() { return Err(RecordBlockError::TooShort(bytes.len())); } let (prefix, payload) = bytes.split_at(magic.len()); if prefix != magic { let mut got = [0u8; 4]; got.copy_from_slice(prefix); return Err(RecordBlockError::BadMagic(got)); } let (record, _) = bincode::serde::decode_from_slice(payload, bincode::config::standard())?; Ok(record) } }