1use blockworx_doc::{document::Document, rev::Rev};
23
24use super::storage::Entry;
25
26use super::assets::{AssetFault, AssetSource, hydrate};
27use super::record::Digest;
28
29pub const REVS: &str = "revs";
31
32pub fn entry(at: Rev) -> Entry {
35 Entry::under(REVS, &format!("{:06}.json.gz", at.get()))
36}
37
38pub trait Backing {
41 fn put(&mut self, at: Rev, bytes: &[u8]) -> std::io::Result<()>;
44
45 fn get(&self, at: Rev) -> std::io::Result<Option<Vec<u8>>>;
51
52 fn names(&self, at: Rev) -> String;
54}
55
56#[derive(Default, Debug)]
63pub struct Memory(std::collections::BTreeMap<Rev, Vec<u8>>);
64
65impl Backing for Memory {
66 fn put(&mut self, at: Rev, bytes: &[u8]) -> std::io::Result<()> {
67 self.0.insert(at, bytes.to_vec());
68 Ok(())
69 }
70
71 fn get(&self, at: Rev) -> std::io::Result<Option<Vec<u8>>> {
72 Ok(self.0.get(&at).cloned())
73 }
74
75 fn names(&self, at: Rev) -> String {
76 format!("rev {} of this session", at.get())
77 }
78}
79
80#[derive(Debug, thiserror::Error)]
83pub enum RevFault {
84 #[error("rev {} was never written to {where_it_would_be}", .at.get())]
85 Missing { at: Rev, where_it_would_be: String },
86 #[error("rev {}'s copy could not be read: {why}", .at.get())]
87 Unreadable { at: Rev, why: std::io::Error },
88 #[error("rev {}'s copy is not a document this build reads: {why}", .at.get())]
89 NotADocument { at: Rev, why: serde_json::Error },
90 #[error("rev {}'s copy holds bytes that hash to {found}, not {named}", .at.get())]
91 Tampered {
92 at: Rev,
93 named: Digest,
94 found: Digest,
95 },
96 #[error(transparent)]
97 Artwork(#[from] AssetFault),
98 #[error("rev {} could not be written: {why}", .at.get())]
99 NotWritten { at: Rev, why: std::io::Error },
100}
101
102pub fn write(
114 into: &mut dyn Backing,
115 at: Rev,
116 document: &Document,
117 payloads: &mut dyn super::assets::AssetSink,
118) -> std::io::Result<Digest> {
119 for (hash, asset) in document.assets() {
120 payloads.put(hash, asset)?;
121 }
122 let bytes = {
123 let _s = tracing::info_span!("encode").entered();
124 encode(&document.without_assets())?
125 };
126 into.put(at, &bytes)?;
127 Ok(Digest::of(&bytes))
128}
129
130pub fn read(from: &dyn Backing, at: Rev) -> Result<Document, RevFault> {
139 if at == Rev::ZERO {
140 return Ok(Document::default());
141 }
142 let bytes = bytes(from, at)?;
143 let json = unpack(&bytes).map_err(|why| RevFault::Unreadable { at, why })?;
144 let mut document: Document =
145 serde_json::from_slice(&json).map_err(|why| RevFault::NotADocument { at, why })?;
146 document.positioned_at(at);
147 Ok(document)
148}
149
150pub fn attached(mut document: Document, assets: &dyn AssetSource) -> Result<Document, AssetFault> {
157 document.attach_assets(|hash| hydrate(assets, hash))?;
158 Ok(document)
159}
160
161pub fn stamp(from: &dyn Backing, at: Rev) -> Result<Digest, RevFault> {
167 if at == Rev::ZERO {
170 return Ok(Digest::of(&[]));
171 }
172 Ok(Digest::of(&bytes(from, at)?))
173}
174
175pub fn bytes(from: &dyn Backing, at: Rev) -> Result<Vec<u8>, RevFault> {
181 from.get(at)
182 .map_err(|why| RevFault::Unreadable { at, why })?
183 .ok_or_else(|| RevFault::Missing {
184 at,
185 where_it_would_be: from.names(at),
186 })
187}
188
189pub fn witnessed(from: &dyn Backing, at: Rev, named: Digest) -> Result<(), RevFault> {
196 let found = stamp(from, at)?;
197 if found == named {
198 Ok(())
199 } else {
200 Err(RevFault::Tampered { at, named, found })
201 }
202}
203
204pub fn through(at: Rev) -> impl Iterator<Item = Rev> {
207 std::iter::successors(Some(Rev::ZERO), |rev| Some(rev.next()))
208 .skip(1)
209 .take_while(move |rev| *rev <= at)
210}
211
212fn encode(document: &Document) -> std::io::Result<Vec<u8>> {
215 let json = {
216 let _s = tracing::info_span!("serialize").entered();
217 serde_json::to_vec(document).map_err(std::io::Error::other)?
218 };
219 let _s = tracing::info_span!("gzip", json = json.len()).entered();
220 pack(&json)
221}
222
223const LEVEL: u32 = 1;
233
234pub(crate) fn pack(json: &[u8]) -> std::io::Result<Vec<u8>> {
235 use std::io::Write as _;
236 let mut gzip = flate2::write::GzEncoder::new(Vec::new(), flate2::Compression::new(LEVEL));
237 gzip.write_all(json)?;
238 gzip.finish()
239}
240
241pub(crate) fn unpack(bytes: &[u8]) -> std::io::Result<Vec<u8>> {
242 use std::io::Read as _;
243 let mut json = Vec::new();
244 flate2::read::GzDecoder::new(bytes).read_to_end(&mut json)?;
245 Ok(json)
246}
247
248#[cfg(test)]
249mod tests {
250 use super::*;
251 use crate::assets::Held;
252 use crate::fixture;
253 use blockworx_doc::fixtures::rev;
254
255 #[test]
258 fn a_rev_round_trips_and_stamps_the_bytes_it_was_written_as() {
259 let document = fixture::documents(2).pop().expect("two documents");
260 let mut backing = Memory::default();
261 let mut payloads = Held::default();
262 let stamped = write(&mut backing, rev(1), &document, &mut payloads).expect("the rev lands");
263
264 assert_eq!(
265 stamp(&backing, rev(1)).expect("the bytes are there"),
266 stamped
267 );
268 assert_eq!(read(&backing, rev(1)).expect("it reads back"), document);
269 witnessed(&backing, rev(1), stamped).expect("the rev is what it says");
270 }
271
272 #[test]
274 fn rev_zero_is_the_empty_document_and_holds_no_bytes() {
275 let backing = Memory::default();
276 assert_eq!(
277 read(&backing, Rev::ZERO).expect("the empty document"),
278 Document::default(),
279 );
280 assert!(matches!(
281 read(&backing, rev(4)),
282 Err(RevFault::Missing { .. }),
283 ));
284 }
285
286 #[test]
289 fn a_rev_file_holds_no_payloads_and_reads_back_the_ones_it_references() {
290 let asset = fixture::svg(1);
291 let document = fixture::with_icon(&asset);
292 assert_eq!(document.assets().count(), 1, "precondition: it holds one");
293
294 let mut backing = Memory::default();
295 let mut payloads = Held::default();
296 write(&mut backing, rev(1), &document, &mut payloads).expect("the rev lands");
297
298 let stripped = read(&backing, rev(1)).expect("it reads back");
299 assert_eq!(stripped.assets().count(), 0, "the rev carries bytes");
300 assert_eq!(
301 attached(stripped, &payloads).expect("the payload is in the store"),
302 document,
303 );
304 }
305
306 #[test]
308 fn bytes_that_are_not_what_the_row_names_are_refused() {
309 let document = fixture::documents(1).pop().expect("one document");
310 let mut backing = Memory::default();
311 let mut payloads = Held::default();
312 let stamped = write(&mut backing, rev(1), &document, &mut payloads).expect("the rev lands");
313
314 backing
315 .put(rev(1), b"not a document")
316 .expect("it is rewritten");
317 assert!(matches!(
318 witnessed(&backing, rev(1), stamped),
319 Err(RevFault::Tampered { .. }),
320 ));
321 assert!(matches!(
322 read(&backing, rev(1)),
323 Err(RevFault::Unreadable { .. } | RevFault::NotADocument { .. }),
324 ));
325 }
326
327 #[test]
328 fn the_revs_through_a_head_start_at_one() {
329 assert_eq!(
330 through(rev(3)).collect::<Vec<_>>(),
331 [rev(1), rev(2), rev(3)]
332 );
333 assert_eq!(through(Rev::ZERO).count(), 0);
334 }
335}