triblespace_core/blob/schemas/
simplearchive.rs1use crate::blob::Blob;
2use crate::blob::BlobSchema;
3use crate::blob::ToBlob;
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::{ConstDescribe, ConstId};
11use crate::repo::BlobStore;
12use crate::trible::Fragment;
13use crate::trible::Trible;
14use crate::trible::TribleSet;
15use crate::value::schemas::hash::Blake3;
16
17use anybytes::Bytes;
18use anybytes::View;
19
20pub struct SimpleArchive;
27
28impl BlobSchema for SimpleArchive {}
29
30impl ConstId for SimpleArchive {
31 const ID: Id = id_hex!("8F4A27C8581DADCBA1ADA8BA228069B6");
32}
33
34impl ConstDescribe for SimpleArchive {
35 fn describe<B>(blobs: &mut B) -> Result<Fragment, B::PutError>
36 where
37 B: BlobStore<Blake3>,
38 {
39 let id = Self::ID;
40 let description = blobs.put(
41 "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.",
42 )?;
43 Ok(entity! {
44 ExclusiveId::force_ref(&id) @
45 metadata::name: blobs.put("simplearchive")?,
46 metadata::description: description,
47 metadata::tag: metadata::KIND_BLOB_SCHEMA,
48 })
49 }
50}
51
52impl ToBlob<SimpleArchive> for TribleSet {
53 fn to_blob(self) -> Blob<SimpleArchive> {
54 let mut tribles: Vec<[u8; 64]> = Vec::with_capacity(self.len());
55 tribles.extend(self.eav.iter_ordered());
56 let bytes: Bytes = tribles.into();
57 Blob::new(bytes)
58 }
59}
60
61impl ToBlob<SimpleArchive> for &TribleSet {
62 fn to_blob(self) -> Blob<SimpleArchive> {
63 let mut tribles: Vec<[u8; 64]> = Vec::with_capacity(self.len());
64 tribles.extend(self.eav.iter_ordered());
65 let bytes: Bytes = tribles.into();
66 Blob::new(bytes)
67 }
68}
69
70#[derive(Debug, Clone, Copy, PartialEq, Eq)]
72pub enum UnarchiveError {
73 BadArchive,
75 BadTrible,
77 BadCanonicalizationRedundancy,
79 BadCanonicalizationOrdering,
81}
82
83impl std::fmt::Display for UnarchiveError {
84 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
85 match self {
86 UnarchiveError::BadArchive => write!(f, "The archive is malformed or invalid."),
87 UnarchiveError::BadTrible => write!(f, "A trible in the archive is malformed."),
88 UnarchiveError::BadCanonicalizationRedundancy => {
89 write!(f, "The archive contains redundant tribles.")
90 }
91 UnarchiveError::BadCanonicalizationOrdering => {
92 write!(f, "The tribles in the archive are not in canonical order.")
93 }
94 }
95 }
96}
97
98impl std::error::Error for UnarchiveError {}
99
100impl TryFromBlob<SimpleArchive> for TribleSet {
101 type Error = UnarchiveError;
102
103 fn try_from_blob(blob: Blob<SimpleArchive>) -> Result<Self, Self::Error> {
104 let mut tribles = TribleSet::new();
105
106 let mut prev_trible = None;
107 let Ok(packed_tribles): Result<View<[[u8; 64]]>, _> = blob.bytes.clone().view() else {
108 return Err(UnarchiveError::BadArchive);
109 };
110 for t in packed_tribles.iter() {
111 if let Some(trible) = Trible::as_transmute_force_raw(t) {
112 if let Some(prev) = prev_trible {
113 if prev == t {
114 return Err(UnarchiveError::BadCanonicalizationRedundancy);
115 }
116 if prev > t {
117 return Err(UnarchiveError::BadCanonicalizationOrdering);
118 }
119 }
120 prev_trible = Some(t);
121 tribles.insert(trible);
122 } else {
123 return Err(UnarchiveError::BadTrible);
124 }
125 }
126
127 Ok(tribles)
128 }
129}