1use std::collections::BTreeSet;
27
28use blockworx_doc::{hash::AssetHash, rev::Rev};
29
30use super::assets::AssetFormat;
31use super::container::{Container, ContainerError, MANIFEST, asset_entry};
32use super::handle::{Clock, Store};
33use super::manifest::{Replays, Row};
34use super::record::Identity;
35use super::revs;
36use super::storage::{Storage, ready_now};
37use super::tags::{Tagging, Tags};
38
39#[derive(Debug, thiserror::Error)]
41pub enum PrefixFailure {
42 #[error("the manifest could not be read: {0}")]
43 Read(std::io::Error),
44 #[error("line {line} of the manifest is not a row this build can read: {why}")]
45 NotARow { line: usize, why: serde_json::Error },
46 #[error("this history has no rev {}", .0.get())]
47 NoSuchRev(Rev),
48 #[error(transparent)]
49 Create(#[from] ContainerError),
50 #[error("{name} could not be carried over: {why}")]
51 Asset { name: String, why: std::io::Error },
52 #[error("rev {}'s copy could not be carried over: {why}", .at.get())]
53 Rev { at: Rev, why: std::io::Error },
54 #[error("the new container's projection did not land: {0}")]
55 Projection(super::Refusal),
56 #[error("rev {}'s name did not come over: {why}", .at.get())]
57 Tag { at: Rev, why: super::Refusal },
58}
59
60pub fn save_through<S: Storage, T: Storage>(
71 source: &S,
72 at: Rev,
73 to: T,
74 mut clock: Clock,
75 by: &Identity,
76) -> Result<Store<T>, PrefixFailure> {
77 let text = read_manifest(source)?;
78 let prefix = through(&text, at)?;
79 let container = Container::create_holding(to, clock.tick(), prefix.as_bytes())?;
80 carry_revs(source, &container, at)?;
81 carry_assets(source, &container, at)?;
82 let mut store = Store::over(container, clock)?;
83 rename_revs(&mut store, &named_through(&text, at)?, by)?;
84 Ok(store)
85}
86
87fn rename_revs<T: Storage>(
94 store: &mut Store<T>,
95 wanted: &Tags,
96 by: &Identity,
97) -> Result<(), PrefixFailure> {
98 let carried: Vec<(Rev, Vec<String>)> = store
99 .tags()
100 .iter()
101 .map(|(rev, names)| (rev, names.to_vec()))
102 .collect();
103 let mut restated: Vec<(Rev, String, Tagging)> = Vec::new();
104 for (rev, names) in &carried {
105 for name in names {
106 if !wanted.of(*rev).contains(name) {
107 restated.push((*rev, name.clone(), Tagging::Removed));
108 }
109 }
110 }
111 for (rev, names) in wanted.iter() {
112 for name in names {
113 if !store.tags().of(rev).contains(name) {
114 restated.push((rev, name.clone(), Tagging::Added));
115 }
116 }
117 }
118 for (rev, name, how) in restated {
119 store
120 .tag(rev, &name, how, by)
121 .map_err(|why| PrefixFailure::Tag { at: rev, why })?;
122 }
123 Ok(())
124}
125
126fn named_through(text: &str, at: Rev) -> Result<Tags, PrefixFailure> {
131 let mut tags = Tags::default();
132 for (nth, line) in whole_lines(text) {
133 let row = parse(nth, line)?;
134 if row.rev <= at
135 && let Replays::Vocabulary(how) = row.kind.replays()
136 {
137 tags.apply(row.rev, &row.label, how);
138 }
139 }
140 Ok(tags)
141}
142
143fn through(text: &str, at: Rev) -> Result<&str, PrefixFailure> {
153 let mut end = 0;
154 let mut reached = Rev::ZERO;
155 for (nth, line) in whole_lines(text) {
156 let row = parse(nth, line)?;
157 if row.kind.takes_a_rev() {
158 if row.rev > at {
159 break;
160 }
161 reached = row.rev;
162 }
163 end += line.len();
164 }
165 if reached != at {
166 return Err(PrefixFailure::NoSuchRev(at));
167 }
168 Ok(&text[..end])
169}
170
171fn parse(nth: usize, line: &str) -> Result<Row, PrefixFailure> {
172 serde_json::from_str(line.trim_end())
173 .map_err(|why| PrefixFailure::NotARow { line: nth + 1, why })
174}
175
176fn whole_lines(text: &str) -> impl Iterator<Item = (usize, &str)> {
180 text.split_inclusive('\n')
181 .take_while(|line| line.ends_with('\n'))
182 .enumerate()
183}
184
185fn read_manifest<S: Storage>(source: &S) -> Result<String, PrefixFailure> {
187 let bytes = ready_now(source.read(&MANIFEST))
188 .map_err(PrefixFailure::Read)?
189 .ok_or_else(|| PrefixFailure::Read(std::io::Error::from(std::io::ErrorKind::NotFound)))?;
190 String::from_utf8(bytes).map_err(|why| {
191 PrefixFailure::Read(std::io::Error::new(
192 std::io::ErrorKind::InvalidData,
193 why.to_string(),
194 ))
195 })
196}
197
198fn carry_revs<S: Storage, T: Storage>(
201 from: &S,
202 to: &Container<T>,
203 at: Rev,
204) -> Result<(), PrefixFailure> {
205 for rev in revs::through(at) {
206 let missing = || std::io::Error::from(std::io::ErrorKind::NotFound);
207 let entry = revs::entry(rev);
208 let carried = ready_now(from.read(&entry))
209 .and_then(|bytes| bytes.ok_or_else(missing))
210 .and_then(|bytes| to.write(&entry, &bytes));
211 carried.map_err(|why| PrefixFailure::Rev { at: rev, why })?;
212 }
213 Ok(())
214}
215
216fn carry_assets<S: Storage, T: Storage>(
225 from: &S,
226 to: &Container<T>,
227 at: Rev,
228) -> Result<(), PrefixFailure> {
229 let backing = to.revs();
230 let mut wanted: BTreeSet<AssetHash> = BTreeSet::new();
231 for rev in revs::through(at) {
232 let Ok(document) = revs::read(&backing, rev) else {
233 continue;
234 };
235 wanted.extend(document.referenced_assets());
236 }
237 for (hash, format) in wanted
238 .into_iter()
239 .flat_map(|hash| AssetFormat::ALL.map(move |format| (hash, format)))
240 {
241 let entry = asset_entry(hash, format);
242 let carried = || -> std::io::Result<()> {
243 if to.exists(&entry)? {
244 return Ok(());
245 }
246 match ready_now(from.read(&entry))? {
247 Some(bytes) => to.write(&entry, &bytes),
248 None => Ok(()),
249 }
250 };
251 carried().map_err(|why| PrefixFailure::Asset {
252 name: format.file_name(hash),
253 why,
254 })?;
255 }
256 Ok(())
257}
258
259#[cfg(test)]
260mod tests {
261 use super::*;
262 use crate::container::ASSETS;
263 use crate::fixture;
264 use crate::record::Identity;
265 use crate::storage::Native;
266 use blockworx_doc::fixtures::rev;
267 use std::path::Path;
268
269 fn source(root: &Path) -> Store<Native> {
272 let mut store = Store::create(Native::at(root), fixture::clock()).expect("the container");
273 let author = Identity::new("ada");
274 for (n, commit) in fixture::edits(3).iter().enumerate() {
275 store
276 .submit_edit(commit.clone(), &author)
277 .expect("the edit lands");
278 if n == 0 {
279 store
280 .tag(rev(1), "the start", Tagging::Added, &author)
281 .expect("the tag");
282 }
283 }
284 store
285 .tag(rev(3), "the end", Tagging::Added, &author)
286 .expect("the tag");
287 store
288 }
289
290 #[test]
294 fn the_saved_log_is_the_sources_own_lines() {
295 let dir = fixture::dir("prefix-bytes");
296 let from = dir.join("source.bwx");
297 drop(source(&from));
298 let to = dir.join("through-2.bwx");
299
300 let saved = save_through(
301 &Native::at(&from),
302 rev(2),
303 Native::at(&to),
304 fixture::clock(),
305 &Identity::new("grace"),
306 )
307 .expect("the prefix saves");
308 drop(saved);
309
310 let whole =
311 std::fs::read_to_string(from.join(MANIFEST.as_str())).expect("the source manifest");
312 let prefix =
313 std::fs::read_to_string(to.join(MANIFEST.as_str())).expect("the saved manifest");
314 assert!(
315 whole.starts_with(&prefix),
316 "the saved log is not a prefix of the one it came from",
317 );
318 assert_eq!(
319 prefix.lines().count(),
320 3,
321 "expected two edits and the tag between them, not {prefix}",
322 );
323 assert!(
324 whole.len() > prefix.len(),
325 "precondition: the source runs past rev 2",
326 );
327 }
328
329 #[test]
333 fn the_saved_container_carries_exactly_the_revs_through_the_cut() {
334 let dir = fixture::dir("prefix-revs");
335 let from = dir.join("source.bwx");
336 drop(source(&from));
337 let to = dir.join("through-2.bwx");
338 assert!(
339 from.join(revs::entry(rev(3)).as_str()).is_file(),
340 "precondition: the source holds a rev past the cut",
341 );
342
343 let saved = save_through(
344 &Native::at(&from),
345 rev(2),
346 Native::at(&to),
347 fixture::clock(),
348 &Identity::new("grace"),
349 )
350 .expect("the prefix saves");
351 drop(saved);
352
353 for at in revs::through(rev(2)) {
354 assert_eq!(
355 std::fs::read(from.join(revs::entry(at).as_str())).expect("the source rev"),
356 std::fs::read(to.join(revs::entry(at).as_str())).expect("the saved rev"),
357 "rev {} did not come over as the bytes it was",
358 at.get(),
359 );
360 }
361 assert!(
362 !to.join(revs::entry(rev(3)).as_str()).exists(),
363 "a rev past the cut came over with the save",
364 );
365 }
366
367 #[test]
370 fn the_saved_container_replays_to_the_rev_it_was_cut_at() {
371 let dir = fixture::dir("prefix-replay");
372 let from = dir.join("source.bwx");
373 let held = source(&from);
374 let at_two = blockworx_doc::repo::Repo::folding(&held.repo().log()[..2])
375 .expect("the prefix folds")
376 .document()
377 .clone();
378 drop(held);
379
380 let to = dir.join("through-2.bwx");
381 let saved = save_through(
382 &Native::at(&from),
383 rev(2),
384 Native::at(&to),
385 fixture::clock(),
386 &Identity::new("grace"),
387 )
388 .expect("the prefix saves");
389 assert_eq!(saved.repo().rev(), rev(2), "the head is not the cut");
390 drop(saved);
391
392 let reopened =
393 Store::open(Native::at(&to), fixture::clock()).expect("the container reopens");
394 assert!(
395 reopened.read_only_reason().is_none(),
396 "the saved container did not verify: {:?}",
397 reopened.read_only_reason(),
398 );
399 assert_eq!(
400 reopened.document().clone(),
401 at_two,
402 "the saved container is not the document that rev held",
403 );
404 }
405
406 #[test]
410 fn a_tag_inside_the_cut_survives_and_one_past_it_does_not() {
411 let dir = fixture::dir("prefix-tags");
412 let from = dir.join("source.bwx");
413 let held = source(&from);
414 assert_eq!(held.tags().len(), 2, "precondition: both revs are named");
415 drop(held);
416
417 let to = dir.join("through-2.bwx");
418 let saved = save_through(
419 &Native::at(&from),
420 rev(2),
421 Native::at(&to),
422 fixture::clock(),
423 &Identity::new("grace"),
424 )
425 .expect("the prefix saves");
426 assert_eq!(saved.tags().of(rev(1)), ["the start"]);
427 assert_eq!(
428 saved.tags().of(rev(3)),
429 Vec::<String>::new(),
430 "a rev past the cut came over with its name",
431 );
432 }
433
434 #[test]
438 fn a_saved_prefix_takes_the_next_edit_normally() {
439 let dir = fixture::dir("prefix-authoring");
440 let from = dir.join("source.bwx");
441 drop(source(&from));
442
443 let to = dir.join("through-1.bwx");
444 let mut saved = save_through(
445 &Native::at(&from),
446 rev(1),
447 Native::at(&to),
448 fixture::clock(),
449 &Identity::new("grace"),
450 )
451 .expect("the prefix");
452 let next = saved
453 .submit_edit(fixture::edits(3)[1].clone(), &Identity::new("grace"))
454 .expect("the edit lands on the saved prefix");
455 assert_eq!(
456 next,
457 rev(2),
458 "the saved container did not author from its own head"
459 );
460 drop(saved);
461
462 let reopened =
463 Store::open(Native::at(&to), fixture::clock()).expect("the container reopens");
464 assert_eq!(reopened.repo().rev(), rev(2));
465 assert!(
466 reopened.read_only_reason().is_none(),
467 "the edit broke the chain"
468 );
469 }
470
471 #[test]
476 fn a_name_given_later_to_a_rev_inside_the_cut_is_written_again() {
477 let dir = fixture::dir("prefix-late-tag");
478 let from = dir.join("source.bwx");
479 {
480 let mut store = source(&from);
481 store
482 .tag(
483 rev(2),
484 "worth keeping",
485 Tagging::Added,
486 &Identity::new("ada"),
487 )
488 .expect("the late tag");
489 }
490
491 let to = dir.join("through-2.bwx");
492 let saved = save_through(
493 &Native::at(&from),
494 rev(2),
495 Native::at(&to),
496 fixture::clock(),
497 &Identity::new("grace"),
498 )
499 .expect("the prefix saves");
500 assert_eq!(
501 saved.tags().of(rev(2)),
502 ["worth keeping"],
503 "a name given after the cut's own rev was left behind with it",
504 );
505 assert_eq!(saved.repo().rev(), rev(2), "a re-written tag spent a rev");
506 drop(saved);
507
508 let reopened =
509 Store::open(Native::at(&to), fixture::clock()).expect("the container reopens");
510 assert!(
511 reopened.read_only_reason().is_none(),
512 "the re-written tag broke the chain: {:?}",
513 reopened.read_only_reason(),
514 );
515 assert_eq!(reopened.tags().of(rev(2)), ["worth keeping"]);
516 }
517
518 #[test]
521 fn a_name_the_source_took_back_does_not_come_over() {
522 let dir = fixture::dir("prefix-untag");
523 let from = dir.join("source.bwx");
524 {
525 let mut store = source(&from);
526 store
527 .tag(rev(1), "the start", Tagging::Removed, &Identity::new("ada"))
528 .expect("the untag");
529 }
530
531 let to = dir.join("through-2.bwx");
532 let saved = save_through(
533 &Native::at(&from),
534 rev(2),
535 Native::at(&to),
536 fixture::clock(),
537 &Identity::new("grace"),
538 )
539 .expect("the prefix saves");
540 assert_eq!(
541 saved.tags().of(rev(1)),
542 Vec::<String>::new(),
543 "the saved container kept a name the source had dropped",
544 );
545 }
546
547 #[test]
552 fn the_artwork_the_prefix_names_comes_with_it() {
553 let dir = fixture::dir("prefix-assets");
554 let from = dir.join("source.bwx");
555 let asset = {
556 let mut store =
557 Store::create(Native::at(&from), fixture::clock()).expect("the container");
558 let author = Identity::new("ada");
559 store
560 .submit_edit(fixture::edits(1)[0].clone(), &author)
561 .expect("the block lands");
562 let asset = fixture::svg(1);
563 store
564 .submit_edit(
565 fixture::commit("Added an icon", fixture::icon(1, &asset)),
566 &author,
567 )
568 .expect("the icon lands");
569 asset
570 };
571 assert_eq!(
572 std::fs::read_dir(from.join(ASSETS))
573 .expect("the source assets")
574 .count(),
575 1,
576 "precondition: the payload was extracted beside the source log",
577 );
578
579 let to = dir.join("through-2.bwx");
580 let saved = save_through(
581 &Native::at(&from),
582 rev(2),
583 Native::at(&to),
584 fixture::clock(),
585 &Identity::new("grace"),
586 )
587 .expect("the prefix saves");
588 assert!(
589 saved.read_only_reason().is_none(),
590 "the saved container could not honour its own artwork: {:?}",
591 saved.read_only_reason(),
592 );
593 drop(saved);
594 let carried: Vec<Vec<u8>> = std::fs::read_dir(to.join(ASSETS))
595 .expect("the saved assets")
596 .map(|entry| std::fs::read(entry.expect("an entry").path()).expect("the payload"))
597 .collect();
598 assert_eq!(carried, vec![asset.bytes().to_vec()]);
599 }
600
601 #[test]
604 fn a_rev_the_log_does_not_hold_is_refused() {
605 let dir = fixture::dir("prefix-no-such-rev");
606 let from = dir.join("source.bwx");
607 drop(source(&from));
608
609 let to = dir.join("through-9.bwx");
610 assert!(matches!(
611 save_through(
612 &Native::at(&from),
613 rev(9),
614 Native::at(&to),
615 fixture::clock(),
616 &Identity::new("grace")
617 ),
618 Err(PrefixFailure::NoSuchRev(_)),
619 ));
620 assert!(!to.exists(), "a refused save left a container behind");
621 }
622
623 #[test]
626 fn cutting_at_the_head_copies_every_line() {
627 let dir = fixture::dir("prefix-whole");
628 let from = dir.join("source.bwx");
629 drop(source(&from));
630
631 let to = dir.join("whole.bwx");
632 let saved = save_through(
633 &Native::at(&from),
634 rev(3),
635 Native::at(&to),
636 fixture::clock(),
637 &Identity::new("grace"),
638 )
639 .expect("the whole log");
640 assert_eq!(saved.repo().rev(), rev(3));
641 assert_eq!(saved.tags().len(), 2, "a whole copy dropped a name");
642 drop(saved);
643 assert_eq!(
644 std::fs::read(from.join(MANIFEST.as_str())).expect("the source manifest"),
645 std::fs::read(to.join(MANIFEST.as_str())).expect("the copy"),
646 "a whole copy is not the same bytes",
647 );
648 }
649}