iris_chat_protocol/storage/
checkpoint.rs1use super::{StorageError, StorageResult};
2use nostr::hashes::{sha256, Hash};
3use std::collections::HashMap;
4use std::sync::Arc;
5
6pub struct StorageCheckpointBody {
10 pub(super) id: [u8; 32],
11 json: Arc<serde_json::value::RawValue>,
12}
13
14impl StorageCheckpointBody {
15 pub fn new(json: Arc<serde_json::value::RawValue>) -> Self {
16 Self {
17 id: sha256::Hash::hash(json.get().as_bytes()).to_byte_array(),
18 json,
19 }
20 }
21 pub fn json(&self) -> &str {
22 self.json.get()
23 }
24}
25
26impl serde::Serialize for StorageCheckpointBody {
27 fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
28 serde::Serialize::serialize(self.json.as_ref(), serializer)
29 }
30}
31
32pub(super) enum PartBytes {
33 Inline(String),
34 Immutable(Arc<StorageCheckpointBody>),
35}
36
37impl PartBytes {
38 pub(super) fn bytes(&self) -> &[u8] {
39 match self {
40 Self::Inline(value) => value.as_bytes(),
41 Self::Immutable(body) => body.json().as_bytes(),
42 }
43 }
44}
45
46pub(super) struct CheckpointPart {
47 pub key: Vec<u8>,
48 pub payload_key: Vec<u8>,
49 pub bytes: PartBytes,
50 pub prefix: String,
51 pub suffix: String,
52 pub padding: usize,
53}
54
55impl CheckpointPart {
56 pub(super) fn len(&self) -> usize {
57 self.prefix
58 .len()
59 .saturating_add(self.bytes.bytes().len())
60 .saturating_add(self.suffix.len())
61 .saturating_add(self.padding)
62 }
63}
64
65pub struct StorageCheckpoint {
68 pub(super) parts: Vec<CheckpointPart>,
69 occurrences: HashMap<(u8, [u8; 32]), u64>,
70 bytes: usize,
71}
72
73impl Default for StorageCheckpoint {
74 fn default() -> Self {
75 Self::new()
76 }
77}
78
79impl StorageCheckpoint {
80 pub fn new() -> Self {
81 Self {
82 parts: Vec::new(),
83 occurrences: HashMap::new(),
84 bytes: 0,
85 }
86 }
87 pub fn len(&self) -> usize {
88 self.bytes
89 }
90 pub fn is_empty(&self) -> bool {
91 self.bytes == 0
92 }
93
94 pub fn push_inline(&mut self, name: &str, text: String, padding: usize) {
95 let mut key = vec![0];
96 key.extend_from_slice(name.as_bytes());
97 let part = CheckpointPart {
98 payload_key: key.clone(),
99 key,
100 bytes: PartBytes::Inline(text),
101 prefix: String::new(),
102 suffix: String::new(),
103 padding,
104 };
105 self.bytes = self.bytes.saturating_add(part.len());
106 self.parts.push(part);
107 }
108
109 pub fn push_record(
110 &mut self,
111 collection: u8,
112 body: Arc<StorageCheckpointBody>,
113 prefix: String,
114 suffix: String,
115 padding: usize,
116 ) {
117 let occurrence = self.occurrences.entry((collection, body.id)).or_default();
118 let mut key = vec![1, collection];
119 key.extend_from_slice(&body.id);
120 key.extend_from_slice(&occurrence.to_be_bytes());
121 *occurrence += 1;
122 let mut payload_key = vec![1];
123 payload_key.extend_from_slice(&body.id);
124 let part = CheckpointPart {
125 key,
126 payload_key,
127 bytes: PartBytes::Immutable(body),
128 prefix,
129 suffix,
130 padding,
131 };
132 self.bytes = self.bytes.saturating_add(part.len());
133 self.parts.push(part);
134 }
135
136 pub fn pad_records(&mut self, page: usize, minimum_record: usize) {
137 let page = page.max(1);
138 for part in &mut self.parts {
139 if matches!(&part.bytes, PartBytes::Immutable(_)) && part.len() >= minimum_record {
140 let padding = part
141 .len()
142 .div_ceil(page)
143 .saturating_mul(page)
144 .saturating_sub(part.len());
145 part.padding = part.padding.saturating_add(padding);
146 self.bytes = self.bytes.saturating_add(padding);
147 }
148 }
149 }
150
151 pub fn materialize(&self) -> StorageResult<String> {
152 self.validate()?;
153 let mut result = String::new();
154 result
155 .try_reserve_exact(self.bytes)
156 .map_err(|error| StorageError::new(error.to_string()))?;
157 for part in &self.parts {
158 result.push_str(&part.prefix);
159 result.push_str(
161 std::str::from_utf8(part.bytes.bytes())
162 .map_err(|_| StorageError::new("invalid checkpoint UTF-8"))?,
163 );
164 result.push_str(&part.suffix);
165 append_padding(&mut result, part.padding);
166 }
167 Ok(result)
168 }
169
170 pub(super) fn validate(&self) -> StorageResult<()> {
171 let total = self.parts.iter().try_fold(0usize, |total, part| {
172 part.prefix
173 .len()
174 .checked_add(part.bytes.bytes().len())
175 .and_then(|n| n.checked_add(part.suffix.len()))
176 .and_then(|n| n.checked_add(part.padding))
177 .and_then(|n| total.checked_add(n))
178 });
179 if total != Some(self.bytes) || i64::try_from(self.bytes).is_err() {
180 return Err(StorageError::new("invalid checkpoint size"));
181 }
182 Ok(())
183 }
184}
185
186pub(super) fn append_padding(output: &mut String, mut bytes: usize) {
187 const SPACES: &str = " ";
188 while bytes >= SPACES.len() {
189 output.push_str(SPACES);
190 bytes -= SPACES.len();
191 }
192 output.push_str(&SPACES[..bytes]);
193}