1use std::io::{Seek, SeekFrom, Write};
48
49use crate::file::{content_hash_parts, FileError};
50use crate::header::{Header, HEADER_LEN, MAGIC};
51use crate::reader::RangeReader;
52
53#[derive(Debug, Clone, PartialEq, Eq)]
55pub struct CardRewritePlan {
56 pub header: Header,
60 pub copies: Vec<(u64, u64)>,
66 pub copies_before_card: usize,
71 pub card_offset: u64,
73 pub new_len: u64,
75}
76
77pub fn plan_card_rewrite(
90 head: &[u8],
91 file_len: u64,
92 metadata_len: u64,
93 build_info_len: u64,
94) -> Result<CardRewritePlan, FileError> {
95 #[derive(Clone, Copy)]
97 enum Slot {
98 Card,
99 Dictionary,
100 Index,
101 Pyramid,
102 Named,
103 Text,
104 Extra(usize),
105 }
106
107 let mut header = Header::from_bytes(head)?;
108 let (m_off, m_len) = (header.metadata_offset, header.metadata_len);
109 let (b_off, b_len) = (header.build_info_offset, header.build_info_len);
110 let (start, end) = match (m_len > 0, b_len > 0) {
111 (true, true) => {
112 if b_off != m_off.saturating_add(m_len) {
113 return Err(FileError::Container(
114 "the build-info section is not adjacent to the metadata section",
115 ));
116 }
117 (m_off, b_off.saturating_add(b_len))
118 }
119 (true, false) => (m_off, m_off.saturating_add(m_len)),
120 (false, true) => (b_off, b_off.saturating_add(b_len)),
121 (false, false) => (HEADER_LEN as u64, HEADER_LEN as u64),
122 };
123 if start < HEADER_LEN as u64 || end > file_len {
124 return Err(FileError::Container(
125 "the card section lies outside the file",
126 ));
127 }
128
129 let mut live: Vec<(u64, u64, Slot)> = vec![(start, end - start, Slot::Card)];
130 let named = [
131 (
132 header.dictionary_offset,
133 header.dictionary_len,
134 Slot::Dictionary,
135 ),
136 (header.root_dir_offset, header.root_dir_len, Slot::Index),
137 (
138 header.pyramid_meta_offset,
139 header.pyramid_meta_len,
140 Slot::Pyramid,
141 ),
142 (
143 header.named_graphs_offset,
144 header.named_graphs_len,
145 Slot::Named,
146 ),
147 (header.text_index_offset, header.text_index_len, Slot::Text),
148 ];
149 let extras = header
150 .extra_sections
151 .iter()
152 .enumerate()
153 .map(|(i, s)| (s.offset, s.length, Slot::Extra(i)));
154 for (off, len, slot) in named.into_iter().chain(extras) {
155 if len == 0 {
156 continue;
157 }
158 if off < HEADER_LEN as u64 || off.checked_add(len).is_none_or(|e| e > file_len) {
159 return Err(FileError::Container("a section overruns the file"));
160 }
161 live.push((off, len, slot));
162 }
163 live.sort_by_key(|&(off, _, slot)| (off, !matches!(slot, Slot::Card)));
165 if live.windows(2).any(|w| w[0].0 + w[0].1 > w[1].0) {
166 return Err(FileError::Container(
167 "a section overlaps the card or another section; this layout cannot be re-carded",
168 ));
169 }
170
171 let new_span = metadata_len.saturating_add(build_info_len);
172 let mut at = HEADER_LEN as u64;
173 let mut copies = Vec::with_capacity(live.len());
174 let (mut card_offset, mut copies_before_card) = (at, 0);
175 for (off, len, slot) in live {
176 let field = match slot {
177 Slot::Card => {
178 card_offset = at;
179 copies_before_card = copies.len();
180 at += new_span;
181 continue;
182 }
183 Slot::Dictionary => &mut header.dictionary_offset,
184 Slot::Index => &mut header.root_dir_offset,
185 Slot::Pyramid => &mut header.pyramid_meta_offset,
186 Slot::Named => &mut header.named_graphs_offset,
187 Slot::Text => &mut header.text_index_offset,
188 Slot::Extra(i) => &mut header.extra_sections[i].offset,
189 };
190 *field = at;
191 copies.push((off, len));
192 at += len;
193 }
194
195 header.metadata_offset = if metadata_len > 0 { card_offset } else { 0 };
196 header.metadata_len = metadata_len;
197 header.build_info_offset = if build_info_len > 0 {
198 card_offset + metadata_len
199 } else {
200 0
201 };
202 header.build_info_len = build_info_len;
203 Ok(CardRewritePlan {
204 header,
205 copies,
206 copies_before_card,
207 card_offset,
208 new_len: at + MAGIC.len() as u64,
209 })
210}
211
212pub const REWRITE_CHUNK: u64 = 8 << 20;
215
216pub fn rewrite_card<R: RangeReader + ?Sized, W: Write + Seek>(
229 src: &R,
230 out: &mut W,
231 metadata: &[u8],
232 build_info: &[u8],
233 progress: &mut dyn FnMut(u64),
234) -> Result<Header, FileError> {
235 rewrite_card_impl(src, out, metadata, build_info, progress, false)
236}
237
238fn rewrite_card_impl<R: RangeReader + ?Sized, W: Write + Seek>(
241 src: &R,
242 out: &mut W,
243 metadata: &[u8],
244 build_info: &[u8],
245 progress: &mut dyn FnMut(u64),
246 force_second_pass: bool,
247) -> Result<Header, FileError> {
248 let file_len = src.len();
249 if file_len < HEADER_LEN as u64 {
250 return Err(FileError::Container("file shorter than its header"));
251 }
252 let head = src.read_at(0, HEADER_LEN as u64)?;
253 let plan = plan_card_rewrite(
254 &head,
255 file_len,
256 metadata.len() as u64,
257 build_info.len() as u64,
258 )?;
259 let mut header = plan.header.clone();
260
261 let parts = content_hash_parts(&header);
264 let streamable = !force_second_pass && parts.windows(2).all(|w| w[0].0 + w[0].1 <= w[1].0);
265 let mut hasher = blake3::Hasher::new();
266 let feed = HashFeed {
267 parts: &parts,
268 next: 0,
269 };
270
271 out.write_all(&header.to_bytes())?;
272 let mut sink = Sink {
273 out: &mut *out,
274 written: HEADER_LEN as u64,
275 streamable,
276 feed,
277 hasher: &mut hasher,
278 };
279 let mut copied = 0u64;
280 for i in 0..=plan.copies.len() {
281 if i == plan.copies_before_card {
282 sink.write(metadata)?;
283 sink.write(build_info)?;
284 }
285 let Some(&(from, len)) = plan.copies.get(i) else {
286 break;
287 };
288 let mut at = from;
289 while at < from + len {
290 let n = (from + len - at).min(REWRITE_CHUNK);
291 let buf = src.read_at(at, n)?;
292 if buf.len() as u64 != n {
293 return Err(FileError::Container("short read while copying"));
294 }
295 sink.write(&buf)?;
296 at += n;
297 copied += n;
298 progress(copied);
299 }
300 }
301 sink.write(&MAGIC)?;
302 let (written, folded) = (sink.written, sink.feed.next);
303 if written != plan.new_len {
304 return Err(FileError::Container(
305 "the rewrite came out the wrong length",
306 ));
307 }
308
309 if !streamable || folded < parts.len() {
310 hasher = blake3::Hasher::new();
313 hasher.update(metadata);
314 let mut old = Header::from_bytes(&head)?;
315 old.metadata_len = 0;
316 for (off, len) in content_hash_parts(&old) {
317 let mut at = off;
318 while at < off + len {
319 let n = (off + len - at).min(REWRITE_CHUNK);
320 hasher.update(&src.read_at(at, n)?);
321 at += n;
322 }
323 }
324 }
325 header
326 .content_hash
327 .copy_from_slice(&hasher.finalize().as_bytes()[..16]);
328 out.seek(SeekFrom::Start(0))?;
329 out.write_all(&header.to_bytes())?;
330 out.flush()?;
331 Ok(header)
332}
333
334#[derive(Debug, Clone, PartialEq, Eq)]
337pub struct CardAppendPlan {
338 pub header: Header,
343 pub card_offset: u64,
347 pub old_len: u64,
350 pub dead_bytes: u64,
353 pub old_head: Vec<u8>,
356}
357
358pub fn plan_card_append(
372 head: &[u8],
373 file_len: u64,
374 metadata_len: u64,
375 in_place: bool,
376) -> Result<CardAppendPlan, FileError> {
377 let mut header = Header::from_bytes(head)?;
378 let old_len = header.expected_file_len().ok_or(FileError::Container(
379 "the header's section directory overflows",
380 ))?;
381 if old_len > file_len {
382 return Err(FileError::Container(
383 "the file is shorter than its header records (truncated?)",
384 ));
385 }
386 let footer = MAGIC.len() as u64;
387 let card_offset = if in_place { old_len } else { old_len - footer };
388 let dead_bytes = header
389 .metadata_len
390 .saturating_add(header.build_info_len)
391 .saturating_add(if in_place { footer } else { 0 });
392 if metadata_len > 0 {
393 header.metadata_offset = card_offset;
394 header.metadata_len = metadata_len;
395 } else {
396 header.metadata_offset = 0;
397 header.metadata_len = 0;
398 }
399 header.build_info_offset = 0;
400 header.build_info_len = 0;
401 Ok(CardAppendPlan {
402 header,
403 card_offset,
404 old_len,
405 dead_bytes,
406 old_head: head[..HEADER_LEN].to_vec(),
407 })
408}
409
410pub fn card_append_hash<R: RangeReader + ?Sized>(
420 src: &R,
421 plan: &CardAppendPlan,
422 metadata: &[u8],
423 progress: &mut dyn FnMut(u64),
424) -> Result<[u8; 16], FileError> {
425 if metadata.len() as u64 != plan.header.metadata_len {
426 return Err(FileError::Container(
427 "the card is not the length the plan was made for",
428 ));
429 }
430 let footer = MAGIC.len() as u64;
431 if src.len() < plan.old_len || src.read_at(plan.old_len - footer, footer)? != MAGIC {
432 return Err(FileError::Container(
433 "the file does not end in its footer where its header says",
434 ));
435 }
436 let mut hasher = blake3::Hasher::new();
437 let mut read = 0u64;
438 for (i, (off, len)) in content_hash_parts(&plan.header).into_iter().enumerate() {
439 if i == 0 && !metadata.is_empty() {
440 hasher.update(metadata);
442 continue;
443 }
444 if off
447 .checked_add(len)
448 .is_none_or(|end| end > plan.old_len - footer)
449 {
450 return Err(FileError::Container("a hashed section overruns the file"));
451 }
452 let mut at = off;
453 while at < off + len {
454 let n = (off + len - at).min(REWRITE_CHUNK);
455 let buf = src.read_at(at, n)?;
456 if buf.len() as u64 != n {
457 return Err(FileError::Container("short read while hashing"));
458 }
459 hasher.update(&buf);
460 at += n;
461 read += n;
462 progress(read);
463 }
464 }
465 let mut hash = [0u8; 16];
466 hash.copy_from_slice(&hasher.finalize().as_bytes()[..16]);
467 Ok(hash)
468}
469
470#[cfg(not(target_arch = "wasm32"))]
483pub fn commit_card_append(
484 file: &mut std::fs::File,
485 plan: &CardAppendPlan,
486 hash: [u8; 16],
487 metadata: &[u8],
488 build_info: &[u8],
489) -> Result<Header, FileError> {
490 commit_card_append_impl(file, plan, hash, metadata, build_info, false)
491}
492
493#[cfg(not(target_arch = "wasm32"))]
496fn commit_card_append_impl(
497 file: &mut std::fs::File,
498 plan: &CardAppendPlan,
499 hash: [u8; 16],
500 metadata: &[u8],
501 build_info: &[u8],
502 fail_before_header: bool,
503) -> Result<Header, FileError> {
504 use std::io::Read;
505 if metadata.len() as u64 != plan.header.metadata_len {
506 return Err(FileError::Container(
507 "the card is not the length the plan was made for",
508 ));
509 }
510 let mut head = vec![0u8; HEADER_LEN];
511 file.seek(SeekFrom::Start(0))?;
512 file.read_exact(&mut head)?;
513 if head != plan.old_head || file.metadata()?.len() < plan.old_len {
514 return Err(FileError::Container(
515 "the file changed since the card was planned; nothing was written",
516 ));
517 }
518 let mut header = plan.header.clone();
519 header.content_hash = hash;
520 let info_offset = plan.card_offset + metadata.len() as u64;
521 if !build_info.is_empty() {
522 header.build_info_offset = info_offset;
523 header.build_info_len = build_info.len() as u64;
524 }
525 let end = info_offset + build_info.len() as u64 + MAGIC.len() as u64;
526 debug_assert_eq!(header.expected_file_len(), Some(end));
527
528 file.seek(SeekFrom::Start(plan.card_offset))?;
531 let mut tail = Vec::with_capacity((end - plan.card_offset) as usize);
532 tail.extend_from_slice(metadata);
533 tail.extend_from_slice(build_info);
534 tail.extend_from_slice(&MAGIC);
535 file.write_all(&tail)?;
536 file.set_len(end)?;
537 file.sync_all()?;
538 if fail_before_header {
539 return Err(FileError::Container("simulated crash before the header"));
540 }
541 file.seek(SeekFrom::Start(0))?;
543 file.write_all(&header.to_bytes())?;
544 file.sync_all()?;
545 Ok(header)
546}
547
548struct Sink<'a, 'p, W> {
551 out: &'a mut W,
552 written: u64,
553 streamable: bool,
554 feed: HashFeed<'p>,
555 hasher: &'a mut blake3::Hasher,
556}
557
558impl<W: Write> Sink<'_, '_, W> {
559 fn write(&mut self, buf: &[u8]) -> Result<(), FileError> {
560 self.out.write_all(buf)?;
561 if self.streamable {
562 self.feed.fold(self.written, buf, self.hasher);
563 }
564 self.written += buf.len() as u64;
565 Ok(())
566 }
567}
568
569struct HashFeed<'a> {
572 parts: &'a [(u64, u64)],
573 next: usize,
574}
575
576impl HashFeed<'_> {
577 fn fold(&mut self, at: u64, buf: &[u8], hasher: &mut blake3::Hasher) {
578 let end = at + buf.len() as u64;
579 while let Some(&(off, len)) = self.parts.get(self.next) {
580 let part_end = off + len;
581 if off >= end {
582 return;
583 }
584 let lo = off.max(at);
585 let hi = part_end.min(end);
586 if lo < hi {
587 hasher.update(&buf[(lo - at) as usize..(hi - at) as usize]);
588 }
589 if part_end <= end {
590 self.next += 1;
591 } else {
592 return;
593 }
594 }
595 }
596}
597
598#[cfg(test)]
599mod tests {
600 use super::*;
601 use crate::card::{derive_card, CardInput};
602 use crate::reader::SliceReader;
603
604 fn q(s: &str, p: &str, o: &str) -> (String, String, String, Option<String>) {
605 (s.into(), p.into(), o.into(), None)
606 }
607
608 fn quads() -> Vec<(String, String, String, Option<String>)> {
609 let ty = "<http://www.w3.org/1999/02/22-rdf-syntax-ns#type>";
610 let mut v = Vec::new();
611 for i in 0..20 {
612 let s = format!("<http://ex/item/{i}>");
613 v.push(q(&s, ty, "<http://ex/Item>"));
614 v.push(q(
615 &s,
616 "<http://ex/next>",
617 &format!("<http://ex/item/{}>", (i + 1) % 20),
618 ));
619 v.push(q(&s, "<http://ex/name>", &format!("\"item {i}\"")));
620 }
621 v.push((
622 "<http://ex/a>".into(),
623 "<http://ex/p>".into(),
624 "<http://ex/b>".into(),
625 Some("<http://ex/g>".into()),
626 ));
627 v
628 }
629
630 fn carded(text_index: bool) -> Vec<u8> {
633 let quads = quads();
634 let card = derive_card(&quads, 50, 1, CardInput::default()).to_json_bytes();
635 let (bytes, _) = crate::ingest::assemble_dataset_with_opts(
636 quads,
637 true,
638 text_index,
639 None,
640 move |_, _| card.clone(),
641 );
642 crate::format::attach_build_info(&bytes, br#"{"schema":1,"builder":"old"}"#).unwrap()
643 }
644
645 fn rewrite(src: &[u8], meta: &[u8], info: &[u8]) -> Vec<u8> {
646 let mut out = std::io::Cursor::new(Vec::new());
647 rewrite_card(&SliceReader::new(src), &mut out, meta, info, &mut |_| {}).unwrap();
648 out.into_inner()
649 }
650
651 #[test]
655 fn a_rewrite_copies_every_other_section_and_rehashes() {
656 for text_index in [false, true] {
657 let src = carded(text_index);
658 let old = Header::from_bytes(&src).unwrap();
659 let meta = br#"{"title":"new card","triple_count":60,"quad_count":61,"named_graph_count":1,"term_count":50,"format_version":5}"#;
660 let info = br#"{"schema":1,"builder":"recard"}"#;
661 let out = rewrite(&src, meta, info);
662 let new = Header::from_bytes(&out).unwrap();
663 assert!(
664 crate::format::verify(&out).unwrap(),
665 "the new hash verifies"
666 );
667 assert_ne!(
668 new.content_hash, old.content_hash,
669 "the card is inside the hash"
670 );
671 let sec = |b: &[u8], off: u64, len: u64| b[off as usize..(off + len) as usize].to_vec();
672 assert_eq!(
673 sec(&out, new.metadata_offset, new.metadata_len),
674 meta.to_vec()
675 );
676 assert_eq!(
677 sec(&out, new.build_info_offset, new.build_info_len),
678 info.to_vec()
679 );
680 for (o, n) in [
681 (
682 (old.dictionary_offset, old.dictionary_len),
683 (new.dictionary_offset, new.dictionary_len),
684 ),
685 (
686 (old.root_dir_offset, old.root_dir_len),
687 (new.root_dir_offset, new.root_dir_len),
688 ),
689 (
690 (old.named_graphs_offset, old.named_graphs_len),
691 (new.named_graphs_offset, new.named_graphs_len),
692 ),
693 (
694 (old.text_index_offset, old.text_index_len),
695 (new.text_index_offset, new.text_index_len),
696 ),
697 ] {
698 assert_eq!(o.1, n.1);
699 assert_eq!(sec(&src, o.0, o.1), sec(&out, n.0, n.1));
700 }
701 assert_eq!(&out[out.len() - 4..], &src[src.len() - 4..]);
703 let spliced = crate::format::attach_build_info(
706 &crate::format::replace_metadata(&src, meta).unwrap(),
707 info,
708 )
709 .unwrap();
710 assert_eq!(out, spliced);
711 let rete = crate::Rete::open(&out).unwrap();
713 assert_eq!(rete.dump(None).len(), 60);
714 }
715 }
716
717 #[test]
721 fn the_second_pass_hash_agrees_with_the_streamed_one() {
722 let src = carded(false);
723 for meta in [&b"{}"[..], &[b'x'; 5000][..]] {
724 let streamed = rewrite(&src, meta, b"{}");
725 let mut out = std::io::Cursor::new(Vec::new());
726 rewrite_card_impl(
727 &SliceReader::new(&src),
728 &mut out,
729 meta,
730 b"{}",
731 &mut |_| {},
732 true,
733 )
734 .unwrap();
735 assert_eq!(out.into_inner(), streamed);
736 }
737 }
738
739 #[test]
742 fn a_rewrite_can_shrink_and_remove() {
743 let src = carded(false);
744 let out = rewrite(&src, b"{}", b"");
745 let h = Header::from_bytes(&out).unwrap();
746 assert_eq!(h.build_info_len, 0);
747 assert!(crate::format::verify(&out).unwrap());
748 let out = rewrite(&src, b"", b"");
749 let h = Header::from_bytes(&out).unwrap();
750 assert_eq!((h.metadata_len, h.build_info_len), (0, 0));
751 assert!(crate::format::verify(&out).unwrap());
752 assert!(crate::Rete::open(&out).is_ok());
753 }
754
755 #[test]
757 fn a_cardless_file_gains_a_card_after_the_header() {
758 let (src, _) =
759 crate::ingest::assemble_dataset_with_opts(quads(), true, false, None, |_, _| {
760 Vec::new()
761 });
762 let out = rewrite(&src, br#"{"title":"t"}"#, br#"{"schema":1}"#);
763 let h = Header::from_bytes(&out).unwrap();
764 assert_eq!(h.metadata_offset, HEADER_LEN as u64);
765 assert_eq!(h.build_info_offset, HEADER_LEN as u64 + h.metadata_len);
766 assert!(crate::format::verify(&out).unwrap());
767 }
768
769 #[test]
772 fn a_detached_build_record_is_refused() {
773 let src = carded(false);
774 let mut h = Header::from_bytes(&src).unwrap();
775 h.build_info_offset += 1;
776 let err = plan_card_rewrite(&h.to_bytes(), src.len() as u64, 10, 10).unwrap_err();
777 assert!(err.to_string().contains("not adjacent"), "{err}");
778 }
779
780 struct Scratch(std::path::PathBuf);
782
783 impl Scratch {
784 fn new(tag: &str, bytes: &[u8]) -> Self {
785 use std::sync::atomic::{AtomicU64, Ordering};
786 static N: AtomicU64 = AtomicU64::new(0);
787 let path = std::env::temp_dir().join(format!(
788 "rete-recard-{tag}-{}-{}.rete",
789 std::process::id(),
790 N.fetch_add(1, Ordering::Relaxed)
791 ));
792 std::fs::write(&path, bytes).unwrap();
793 Scratch(path)
794 }
795
796 fn bytes(&self) -> Vec<u8> {
797 std::fs::read(&self.0).unwrap()
798 }
799
800 fn open(&self) -> std::fs::File {
801 std::fs::OpenOptions::new()
802 .read(true)
803 .write(true)
804 .open(&self.0)
805 .unwrap()
806 }
807 }
808
809 impl Drop for Scratch {
810 fn drop(&mut self) {
811 let _ = std::fs::remove_file(&self.0);
812 }
813 }
814
815 fn append(file: &Scratch, meta: &[u8], info: &[u8], in_place: bool) -> Header {
818 let bytes = file.bytes();
819 let plan =
820 plan_card_append(&bytes, bytes.len() as u64, meta.len() as u64, in_place).unwrap();
821 let hash = card_append_hash(&SliceReader::new(&bytes), &plan, meta, &mut |_| {}).unwrap();
822 commit_card_append(&mut file.open(), &plan, hash, meta, info).unwrap()
823 }
824
825 const META: &[u8] = br#"{"title":"after the data","triple_count":60,"quad_count":61,"named_graph_count":1,"term_count":50,"format_version":5}"#;
826 const INFO: &[u8] = br#"{"schema":1,"builder":"in place"}"#;
827
828 #[test]
833 fn a_card_appended_in_place_moves_nothing_and_verifies() {
834 for text_index in [false, true] {
835 let src = carded(text_index);
836 let old = Header::from_bytes(&src).unwrap();
837 let file = Scratch::new("inplace", &src);
838 let new = append(&file, META, INFO, true);
839 let out = file.bytes();
840
841 assert_eq!(
842 new.metadata_offset,
843 src.len() as u64,
844 "after the old footer"
845 );
846 assert_eq!(
847 new.build_info_offset,
848 new.metadata_offset + META.len() as u64
849 );
850 assert_eq!(new.expected_file_len(), Some(out.len() as u64));
851 assert_eq!(&out[out.len() - 4..], &MAGIC);
852 assert_eq!(
853 &out[..],
854 &{
855 let mut want = src[..].to_vec();
856 want[..HEADER_LEN].copy_from_slice(&new.to_bytes());
857 want.extend_from_slice(META);
858 want.extend_from_slice(INFO);
859 want.extend_from_slice(&MAGIC);
860 want
861 }[..]
862 );
863 for (o, n) in [
864 (old.dictionary_offset, new.dictionary_offset),
865 (old.root_dir_offset, new.root_dir_offset),
866 (old.named_graphs_offset, new.named_graphs_offset),
867 (old.text_index_offset, new.text_index_offset),
868 ] {
869 assert_eq!(o, n, "a data section moved");
870 }
871 assert!(crate::format::verify(&out).unwrap());
872 let copied = rewrite(&src, META, INFO);
873 assert_eq!(
874 new.content_hash,
875 Header::from_bytes(&copied).unwrap().content_hash,
876 "the hash does not depend on where the card lies"
877 );
878 let (m, b) =
879 crate::file::read_card_and_build_info_ranged(&SliceReader::new(&out)).unwrap();
880 assert_eq!((m.unwrap(), b.unwrap()), (META.to_vec(), INFO.to_vec()));
881 let rete = crate::Rete::open(&out).unwrap();
882 assert_eq!(rete.metadata(), Some(META));
883 assert_eq!(rete.dump(None).len(), 60);
884 }
885 }
886
887 #[test]
892 fn a_crash_before_the_header_leaves_the_old_file() {
893 let src = carded(false);
894 let file = Scratch::new("crash", &src);
895 let plan = plan_card_append(&src, src.len() as u64, META.len() as u64, true).unwrap();
896 let hash = card_append_hash(&SliceReader::new(&src), &plan, META, &mut |_| {}).unwrap();
897 let err =
898 commit_card_append_impl(&mut file.open(), &plan, hash, META, INFO, true).unwrap_err();
899 assert!(err.to_string().contains("simulated"), "{err}");
900
901 let crashed = file.bytes();
902 assert!(crashed.len() > src.len(), "the new sections were written");
903 assert_eq!(&crashed[..src.len()], &src[..], "the old file is untouched");
904 assert!(crate::format::verify(&crashed).unwrap());
905 let rete = crate::Rete::open(&crashed).unwrap();
906 assert_eq!(
907 rete.metadata(),
908 crate::Rete::open(&src).unwrap().metadata(),
909 "the old card"
910 );
911 assert_eq!(rete.dump(None).len(), 60);
912
913 let rerun = append(&file, META, INFO, true);
916 let clean = Scratch::new("clean", &src);
917 append(&clean, META, INFO, true);
918 assert_eq!(file.bytes(), clean.bytes());
919 assert!(crate::format::verify(&file.bytes()).unwrap());
920 assert_eq!(rerun.expected_file_len(), Some(file.bytes().len() as u64));
921 }
922
923 #[test]
927 fn a_fresh_append_replaces_the_footer() {
928 let (src, _) =
929 crate::ingest::assemble_dataset_with_opts(quads(), true, false, None, |_, _| {
930 Vec::new()
931 });
932 let file = Scratch::new("fresh", &src);
933 let plan = plan_card_append(&src, src.len() as u64, META.len() as u64, false).unwrap();
934 assert_eq!(plan.dead_bytes, 0);
935 let h = append(&file, META, INFO, false);
936 let out = file.bytes();
937 assert_eq!(h.metadata_offset, src.len() as u64 - 4);
938 assert_eq!(out.len(), src.len() + META.len() + INFO.len());
939 assert!(crate::format::verify(&out).unwrap());
940 assert_eq!(
942 Header::from_bytes(&rewrite(&src, META, INFO))
943 .unwrap()
944 .content_hash,
945 h.content_hash
946 );
947 }
948
949 #[test]
954 fn a_card_after_the_data_can_be_replaced_again_every_way() {
955 let src = carded(true);
956 let file = Scratch::new("again", &src);
957 append(&file, META, INFO, true);
958 let once = file.bytes();
959 let plan = plan_card_append(&once, once.len() as u64, 2, true).unwrap();
960 assert_eq!(
961 plan.dead_bytes,
962 (META.len() + INFO.len() + 4) as u64,
963 "the first in-place card, its record and the footer before it"
964 );
965 append(&file, b"{}", b"{\"schema\":1}", true);
966 assert!(crate::format::verify(&file.bytes()).unwrap());
967
968 let meta2 = br#"{"title":"copied"}"#;
971 let copied = rewrite(&once, meta2, INFO);
972 let h = Header::from_bytes(&copied).unwrap();
973 assert!(h.metadata_offset > h.dictionary_offset);
974 let old = Header::from_bytes(&src).unwrap();
975 assert_eq!(
976 copied.len() as u64,
977 src.len() as u64 - old.metadata_len - old.build_info_len
978 + (meta2.len() + INFO.len()) as u64
979 );
980 assert_eq!(h.expected_file_len(), Some(copied.len() as u64));
981 assert!(crate::format::verify(&copied).unwrap());
982 assert_eq!(crate::Rete::open(&copied).unwrap().dump(None).len(), 60);
983 assert_eq!(h.dictionary_offset, HEADER_LEN as u64);
985 let sec = |b: &[u8], off: u64, len: u64| b[off as usize..(off + len) as usize].to_vec();
986 assert_eq!(
987 sec(&copied, h.root_dir_offset, h.root_dir_len),
988 sec(&src, old.root_dir_offset, old.root_dir_len)
989 );
990
991 let swapped = crate::format::attach_build_info(&once, b"{\"schema\":1,\"x\":2}").unwrap();
993 let h = Header::from_bytes(&swapped).unwrap();
994 assert_eq!(h.build_info_offset, h.metadata_offset + h.metadata_len);
995 assert_eq!(h.expected_file_len(), Some(swapped.len() as u64));
996 assert!(crate::format::verify(&swapped).unwrap());
997 }
998
999 #[test]
1002 fn an_append_refuses_what_it_cannot_vouch_for() {
1003 let src = carded(false);
1004 let err = plan_card_append(&src, src.len() as u64 - 1, 10, true).unwrap_err();
1005 assert!(err.to_string().contains("shorter"), "{err}");
1006
1007 let mut broken = src.clone();
1008 let n = broken.len();
1009 broken[n - 1] = b'X';
1010 let plan = plan_card_append(&broken, n as u64, META.len() as u64, true).unwrap();
1011 let err =
1012 card_append_hash(&SliceReader::new(&broken), &plan, META, &mut |_| {}).unwrap_err();
1013 assert!(err.to_string().contains("footer"), "{err}");
1014
1015 let file = Scratch::new("changed", &src);
1016 let plan = plan_card_append(&src, src.len() as u64, META.len() as u64, true).unwrap();
1017 let hash = card_append_hash(&SliceReader::new(&src), &plan, META, &mut |_| {}).unwrap();
1018 append(&file, b"{}", b"", true); let before = file.bytes();
1020 let err = commit_card_append(&mut file.open(), &plan, hash, META, INFO).unwrap_err();
1021 assert!(err.to_string().contains("changed"), "{err}");
1022 assert_eq!(file.bytes(), before, "nothing was written");
1023 }
1024
1025 #[test]
1028 fn the_hash_feed_folds_parts_across_write_boundaries() {
1029 let data: Vec<u8> = (0..200u8).collect();
1030 let parts = [(10u64, 30u64), (40, 5), (100, 50)];
1031 let mut want = blake3::Hasher::new();
1032 for &(o, l) in &parts {
1033 want.update(&data[o as usize..(o + l) as usize]);
1034 }
1035 let mut got = blake3::Hasher::new();
1036 let mut feed = HashFeed {
1037 parts: &parts,
1038 next: 0,
1039 };
1040 for (i, chunk) in data.chunks(7).enumerate() {
1041 feed.fold(i as u64 * 7, chunk, &mut got);
1042 }
1043 assert_eq!(want.finalize(), got.finalize());
1044 assert_eq!(feed.next, parts.len());
1045 }
1046}