1use blockworx_doc::rev::Rev;
20
21use crate::container::MANIFEST;
22use crate::manifest::{self, End, Row};
23use crate::record::Digest;
24use crate::revs::{REVS, entry, pack};
25use crate::storage::{Entry, Storage, ready_now};
26
27fn zstd_entry(at: Rev) -> Entry {
29 Entry::under(REVS, &format!("{:06}.json.zst", at.get()))
30}
31
32#[derive(Clone, Copy, PartialEq, Eq, Debug)]
34pub enum Migrated {
35 AlreadyDone,
39 Rewrote {
40 revs: usize,
41 },
42}
43
44impl std::fmt::Display for Migrated {
45 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
46 match self {
47 Migrated::AlreadyDone => write!(f, "already gzip: nothing to do"),
48 Migrated::Rewrote { revs } => {
49 write!(
50 f,
51 "{revs} revs rewritten as gzip, and the manifest re-stamped"
52 )
53 }
54 }
55 }
56}
57
58#[derive(Debug, thiserror::Error)]
59pub enum MigrationFailure {
60 #[error("the manifest could not be read: {0}")]
61 Read(std::io::Error),
62 #[error("the manifest does not verify at {0} — migrating it would set the break in stone")]
63 Broken(manifest::BreakReport),
64 #[error("rev {} is neither gzip nor zstd: there is nothing under either name", .0.get())]
65 NoSuchRev(Rev),
66 #[error("rev {}'s bytes are not zstd: {why}", .at.get())]
67 NotZstd { at: Rev, why: std::io::Error },
68 #[error("rev {} could not be written: {why}", .at.get())]
69 NotWritten { at: Rev, why: std::io::Error },
70 #[error("the re-stamped manifest did not land: {0}")]
71 NotStamped(std::io::Error),
72 #[error("the migrated container did not reopen: {0}")]
73 Reopen(String),
74}
75
76pub fn to_gzip<S: Storage>(storage: &S) -> Result<Migrated, MigrationFailure> {
89 let text = read_manifest(storage)?;
90 let scanned = manifest::scan(&text);
91 if let End::Broken(report) = scanned.end {
92 return Err(MigrationFailure::Broken(report));
93 }
94 let mut rows: Vec<Row> = scanned
95 .rows
96 .into_iter()
97 .map(|verified| verified.row)
98 .collect();
99 let mut rewritten = 0;
100 let mut stamped: Vec<(Rev, Digest)> = Vec::new();
101 for at in rows
102 .iter()
103 .filter(|row| row.kind.takes_a_rev())
104 .map(|row| row.rev)
105 {
106 let (digest, moved) = carry_over(storage, at)?;
107 rewritten += usize::from(moved);
108 stamped.push((at, digest));
109 }
110 restamp(&mut rows, &stamped);
111
112 let text_now = lines(&rows);
113 if rewritten == 0 && text_now == text {
114 return Ok(Migrated::AlreadyDone);
115 }
116 ready_now(storage.write(&MANIFEST, text_now.as_bytes()))
117 .map_err(MigrationFailure::NotStamped)?;
118 Ok(Migrated::Rewrote { revs: rewritten })
119}
120
121fn carry_over<S: Storage>(storage: &S, at: Rev) -> Result<(Digest, bool), MigrationFailure> {
124 let old = zstd_entry(at);
125 let new = entry(at);
126 let Some(bytes) = read(storage, &old, at)? else {
127 let held = read(storage, &new, at)?.ok_or(MigrationFailure::NoSuchRev(at))?;
129 return Ok((Digest::of(&held), false));
130 };
131 let json =
132 zstd::decode_all(bytes.as_slice()).map_err(|why| MigrationFailure::NotZstd { at, why })?;
133 let packed = pack(&json).map_err(|why| MigrationFailure::NotWritten { at, why })?;
134 let written =
135 ready_now(storage.write(&new, &packed)).and_then(|()| ready_now(storage.remove(&old)));
136 written.map_err(|why| MigrationFailure::NotWritten { at, why })?;
137 Ok((Digest::of(&packed), true))
138}
139
140fn read<S: Storage>(
141 storage: &S,
142 at: &Entry,
143 rev: Rev,
144) -> Result<Option<Vec<u8>>, MigrationFailure> {
145 ready_now(storage.read(at)).map_err(|why| MigrationFailure::NotWritten { at: rev, why })
146}
147
148fn restamp(rows: &mut [Row], stamped: &[(Rev, Digest)]) {
156 let mut head = Digest::of(&[]);
157 let mut parent = Digest::genesis();
158 for row in rows.iter_mut() {
159 if row.kind.takes_a_rev()
160 && let Some((_, digest)) = stamped.iter().find(|(rev, _)| *rev == row.rev)
161 {
162 head = *digest;
163 }
164 row.hash = head;
165 row.parent = parent;
166 parent = row.digest();
167 }
168}
169
170fn lines(rows: &[Row]) -> String {
171 let mut text = String::new();
172 for row in rows {
173 text.push_str(&String::from_utf8_lossy(&row.canonical_bytes()));
174 text.push('\n');
175 }
176 text
177}
178
179fn read_manifest<S: Storage>(storage: &S) -> Result<String, MigrationFailure> {
180 let bytes = ready_now(storage.read(&MANIFEST))
181 .map_err(MigrationFailure::Read)?
182 .ok_or_else(|| {
183 MigrationFailure::Read(std::io::Error::from(std::io::ErrorKind::NotFound))
184 })?;
185 String::from_utf8(bytes).map_err(|why| {
186 MigrationFailure::Read(std::io::Error::new(
187 std::io::ErrorKind::InvalidData,
188 why.to_string(),
189 ))
190 })
191}
192
193#[cfg(test)]
194mod tests {
195 use super::*;
196 use crate::fixture;
197 use crate::handle::Store;
198 use crate::storage::Native;
199 use crate::tags::Tagging;
200
201 #[test]
205 fn a_zstd_container_comes_over_and_still_verifies() {
206 let dir = fixture::dir("migrate-gzip");
207 let root = dir.join("doc.bwx");
208 let document = {
209 let mut store =
210 Store::create(Native::at(&root), fixture::clock()).expect("the container");
211 for commit in fixture::edits(3) {
212 store
213 .submit_edit(commit, &fixture::author())
214 .expect("the edit lands");
215 }
216 store
217 .tag(
218 blockworx_doc::fixtures::rev(2),
219 "Initial Draft",
220 Tagging::Added,
221 &fixture::author(),
222 )
223 .expect("the tag lands");
224 store.document().clone()
225 };
226 roll_back_to_zstd(&root);
227
228 let migrated = to_gzip(&Native::at(&root)).expect("the container comes over");
229 assert_eq!(migrated, Migrated::Rewrote { revs: 3 });
230
231 for at in crate::revs::through(blockworx_doc::fixtures::rev(3)) {
232 assert!(
233 root.join(entry(at).as_str()).is_file(),
234 "rev {} was not written as gzip",
235 at.get(),
236 );
237 assert!(
238 !root.join(zstd_entry(at).as_str()).exists(),
239 "rev {}'s old file was left behind",
240 at.get(),
241 );
242 }
243
244 let reopened = Store::open(Native::at(&root), fixture::clock()).expect("it reopens");
245 assert!(
246 reopened.read_only_reason().is_none(),
247 "the migrated manifest does not verify: {:?}",
248 reopened.read_only_reason(),
249 );
250 assert_eq!(reopened.document().clone(), document);
251 assert_eq!(
252 reopened.tags().of(blockworx_doc::fixtures::rev(2)),
253 ["Initial Draft"],
254 "the tag row did not come over",
255 );
256 assert!(
257 crate::dump::verify(Native::at(&root))
258 .expect("the manifest reads")
259 .broken
260 .is_none(),
261 "the fsck does not accept the migrated container",
262 );
263 }
264
265 #[test]
267 fn a_container_already_gzip_is_left_alone() {
268 let dir = fixture::dir("migrate-idempotent");
269 let root = dir.join("doc.bwx");
270 {
271 let mut store =
272 Store::create(Native::at(&root), fixture::clock()).expect("the container");
273 for commit in fixture::edits(2) {
274 store
275 .submit_edit(commit, &fixture::author())
276 .expect("the edit lands");
277 }
278 }
279 let before = std::fs::read(root.join(MANIFEST.as_str())).expect("the manifest");
280
281 assert_eq!(
282 to_gzip(&Native::at(&root)).expect("it answers"),
283 Migrated::AlreadyDone,
284 );
285 assert_eq!(
286 std::fs::read(root.join(MANIFEST.as_str())).expect("the manifest"),
287 before,
288 "a container with nothing to migrate had its manifest rewritten",
289 );
290 }
291
292 #[test]
295 fn a_half_converted_container_is_finished_by_the_next_run() {
296 let dir = fixture::dir("migrate-half");
297 let root = dir.join("doc.bwx");
298 {
299 let mut store =
300 Store::create(Native::at(&root), fixture::clock()).expect("the container");
301 for commit in fixture::edits(3) {
302 store
303 .submit_edit(commit, &fixture::author())
304 .expect("the edit lands");
305 }
306 }
307 roll_back_to_zstd(&root);
308 let at = blockworx_doc::fixtures::rev(1);
311 let json = zstd::decode_all(
312 std::fs::read(root.join(zstd_entry(at).as_str()))
313 .expect("the old rev")
314 .as_slice(),
315 )
316 .expect("it decodes");
317 std::fs::write(
318 root.join(entry(at).as_str()),
319 pack(&json).expect("it packs"),
320 )
321 .expect("the new rev");
322 std::fs::remove_file(root.join(zstd_entry(at).as_str())).expect("the old rev goes");
323
324 assert_eq!(
325 to_gzip(&Native::at(&root)).expect("the rest comes over"),
326 Migrated::Rewrote { revs: 2 },
327 );
328 let reopened = Store::open(Native::at(&root), fixture::clock()).expect("it reopens");
329 assert!(reopened.read_only_reason().is_none());
330 }
331
332 #[test]
335 fn a_broken_manifest_is_refused() {
336 let dir = fixture::dir("migrate-broken");
337 let root = dir.join("doc.bwx");
338 {
339 let mut store =
340 Store::create(Native::at(&root), fixture::clock()).expect("the container");
341 for commit in fixture::edits(3) {
342 store
343 .submit_edit(commit, &fixture::author())
344 .expect("the edit lands");
345 }
346 }
347 let mut lines = crate::tests::manifest_lines(&root);
348 lines[1] = lines[1].replace("Added a block", "Added a blork");
349 crate::tests::write_manifest(&root, &lines);
350
351 assert!(matches!(
352 to_gzip(&Native::at(&root)),
353 Err(MigrationFailure::Broken(_)),
354 ));
355 }
356
357 fn roll_back_to_zstd(root: &std::path::Path) {
360 let text = std::fs::read_to_string(root.join(MANIFEST.as_str())).expect("the manifest");
361 let mut rows: Vec<Row> = manifest::scan(&text)
362 .rows
363 .into_iter()
364 .map(|verified| verified.row)
365 .collect();
366 let mut stamped: Vec<(Rev, Digest)> = Vec::new();
367 for at in rows
368 .iter()
369 .filter(|row| row.kind.takes_a_rev())
370 .map(|row| row.rev)
371 {
372 let gz = root.join(entry(at).as_str());
373 let json = crate::revs::unpack(&std::fs::read(&gz).expect("the gzip rev"))
374 .expect("it decodes");
375 let packed = zstd::encode_all(json.as_slice(), 1).expect("it compresses");
376 std::fs::write(root.join(zstd_entry(at).as_str()), &packed).expect("the zstd rev");
377 std::fs::remove_file(&gz).expect("the gzip rev goes");
378 stamped.push((at, Digest::of(&packed)));
379 }
380 restamp(&mut rows, &stamped);
381 std::fs::write(root.join(MANIFEST.as_str()), lines(&rows)).expect("the manifest");
382 }
383}