Skip to main content

triblespace_core/blob/encodings/
simplearchive.rs

1use crate::inline::Encodes;
2use crate::blob::Blob;
3use crate::blob::BlobEncoding;
4use crate::blob::TryFromBlob;
5use crate::id::ExclusiveId;
6use crate::id::Id;
7use crate::id_hex;
8use crate::macros::entity;
9use crate::metadata;
10use crate::metadata::MetaDescribe;
11use crate::trible::Fragment;
12use crate::trible::Trible;
13use crate::trible::TribleSet;
14
15use anybytes::Bytes;
16use anybytes::View;
17
18/// Canonical trible sequence stored as raw 64-byte entries.
19///
20/// The simplest portable archive format — a flat byte array of tribles
21/// in canonical EAV order with no compression. Used for commits,
22/// streaming, hashing, and audit trails where byte-for-byte stability
23/// matters.
24pub struct SimpleArchive;
25
26impl BlobEncoding for SimpleArchive {}
27
28impl MetaDescribe for SimpleArchive {
29    fn describe() -> Fragment {
30        let id: Id = id_hex!("8F4A27C8581DADCBA1ADA8BA228069B6");
31        entity! {
32            ExclusiveId::force_ref(&id) @
33                metadata::name: "simplearchive",
34                metadata::description: "Canonical trible sequence stored as raw 64-byte entries. This is the simplest portable archive format and preserves the exact trible ordering expected by the canonicalization rules.\n\nUse SimpleArchive for export, import, streaming, hashing, or audit trails where you want a byte-for-byte stable representation. Prefer SuccinctArchiveBlob when you need compact indexed storage and fast offline queries, and keep a SimpleArchive around if you want a source of truth that can be re-indexed or validated.",
35                metadata::tag: metadata::KIND_BLOB_ENCODING,
36        }
37    }
38}
39
40impl Encodes<TribleSet> for SimpleArchive
41where crate::inline::encodings::hash::Handle<SimpleArchive>: crate::inline::InlineEncoding,
42{
43    type Output = Blob<SimpleArchive>;
44    fn encode(source: TribleSet) -> Blob<SimpleArchive> {
45        let mut tribles: Vec<[u8; 64]> = Vec::with_capacity(source.len());
46        tribles.extend(source.eav.iter_ordered());
47        let bytes: Bytes = tribles.into();
48        Blob::new(bytes)
49    }
50}
51
52impl Encodes<&TribleSet> for SimpleArchive
53where crate::inline::encodings::hash::Handle<SimpleArchive>: crate::inline::InlineEncoding,
54{
55    type Output = Blob<SimpleArchive>;
56    fn encode(source: &TribleSet) -> Blob<SimpleArchive> {
57        let mut tribles: Vec<[u8; 64]> = Vec::with_capacity(source.len());
58        tribles.extend(source.eav.iter_ordered());
59        let bytes: Bytes = tribles.into();
60        Blob::new(bytes)
61    }
62}
63
64/// Error returned when deserializing a [`SimpleArchive`] blob into a [`TribleSet`].
65#[derive(Debug, Clone, Copy, PartialEq, Eq)]
66pub enum UnarchiveError {
67    /// The blob length is not a multiple of 64 bytes.
68    BadArchive,
69    /// A 64-byte entry has a nil entity or attribute.
70    BadTrible,
71    /// The archive contains duplicate tribles.
72    BadCanonicalizationRedundancy,
73    /// The tribles are not in ascending canonical order.
74    BadCanonicalizationOrdering,
75}
76
77impl std::fmt::Display for UnarchiveError {
78    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
79        match self {
80            UnarchiveError::BadArchive => write!(f, "The archive is malformed or invalid."),
81            UnarchiveError::BadTrible => write!(f, "A trible in the archive is malformed."),
82            UnarchiveError::BadCanonicalizationRedundancy => {
83                write!(f, "The archive contains redundant tribles.")
84            }
85            UnarchiveError::BadCanonicalizationOrdering => {
86                write!(f, "The tribles in the archive are not in canonical order.")
87            }
88        }
89    }
90}
91
92impl std::error::Error for UnarchiveError {}
93
94impl TryFromBlob<SimpleArchive> for TribleSet {
95    type Error = UnarchiveError;
96
97    fn try_from_blob(blob: Blob<SimpleArchive>) -> Result<Self, Self::Error> {
98        let mut tribles = TribleSet::new();
99
100        let mut prev_trible = None;
101        let Ok(packed_tribles): Result<View<[[u8; 64]]>, _> = blob.bytes.clone().view() else {
102            return Err(UnarchiveError::BadArchive);
103        };
104        for t in packed_tribles.iter() {
105            if let Some(trible) = Trible::as_transmute_force_raw(t) {
106                if let Some(prev) = prev_trible {
107                    if prev == t {
108                        return Err(UnarchiveError::BadCanonicalizationRedundancy);
109                    }
110                    if prev > t {
111                        return Err(UnarchiveError::BadCanonicalizationOrdering);
112                    }
113                }
114                prev_trible = Some(t);
115                tribles.insert(trible);
116            } else {
117                return Err(UnarchiveError::BadTrible);
118            }
119        }
120
121        Ok(tribles)
122    }
123}