use std::io::{Seek, SeekFrom, Write};
use crate::file::{content_hash_parts, FileError};
use crate::header::{Header, HEADER_LEN, MAGIC};
use crate::reader::RangeReader;
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct CardRewritePlan {
pub header: Header,
pub copies: Vec<(u64, u64)>,
pub copies_before_card: usize,
pub card_offset: u64,
pub new_len: u64,
}
pub fn plan_card_rewrite(
head: &[u8],
file_len: u64,
metadata_len: u64,
build_info_len: u64,
) -> Result<CardRewritePlan, FileError> {
#[derive(Clone, Copy)]
enum Slot {
Card,
Dictionary,
Index,
Pyramid,
Named,
Text,
Extra(usize),
}
let mut header = Header::from_bytes(head)?;
let (m_off, m_len) = (header.metadata_offset, header.metadata_len);
let (b_off, b_len) = (header.build_info_offset, header.build_info_len);
let (start, end) = match (m_len > 0, b_len > 0) {
(true, true) => {
if b_off != m_off.saturating_add(m_len) {
return Err(FileError::Container(
"the build-info section is not adjacent to the metadata section",
));
}
(m_off, b_off.saturating_add(b_len))
}
(true, false) => (m_off, m_off.saturating_add(m_len)),
(false, true) => (b_off, b_off.saturating_add(b_len)),
(false, false) => (HEADER_LEN as u64, HEADER_LEN as u64),
};
if start < HEADER_LEN as u64 || end > file_len {
return Err(FileError::Container(
"the card section lies outside the file",
));
}
let mut live: Vec<(u64, u64, Slot)> = vec![(start, end - start, Slot::Card)];
let named = [
(
header.dictionary_offset,
header.dictionary_len,
Slot::Dictionary,
),
(header.root_dir_offset, header.root_dir_len, Slot::Index),
(
header.pyramid_meta_offset,
header.pyramid_meta_len,
Slot::Pyramid,
),
(
header.named_graphs_offset,
header.named_graphs_len,
Slot::Named,
),
(header.text_index_offset, header.text_index_len, Slot::Text),
];
let extras = header
.extra_sections
.iter()
.enumerate()
.map(|(i, s)| (s.offset, s.length, Slot::Extra(i)));
for (off, len, slot) in named.into_iter().chain(extras) {
if len == 0 {
continue;
}
if off < HEADER_LEN as u64 || off.checked_add(len).is_none_or(|e| e > file_len) {
return Err(FileError::Container("a section overruns the file"));
}
live.push((off, len, slot));
}
live.sort_by_key(|&(off, _, slot)| (off, !matches!(slot, Slot::Card)));
if live.windows(2).any(|w| w[0].0 + w[0].1 > w[1].0) {
return Err(FileError::Container(
"a section overlaps the card or another section; this layout cannot be re-carded",
));
}
let new_span = metadata_len.saturating_add(build_info_len);
let mut at = HEADER_LEN as u64;
let mut copies = Vec::with_capacity(live.len());
let (mut card_offset, mut copies_before_card) = (at, 0);
for (off, len, slot) in live {
let field = match slot {
Slot::Card => {
card_offset = at;
copies_before_card = copies.len();
at += new_span;
continue;
}
Slot::Dictionary => &mut header.dictionary_offset,
Slot::Index => &mut header.root_dir_offset,
Slot::Pyramid => &mut header.pyramid_meta_offset,
Slot::Named => &mut header.named_graphs_offset,
Slot::Text => &mut header.text_index_offset,
Slot::Extra(i) => &mut header.extra_sections[i].offset,
};
*field = at;
copies.push((off, len));
at += len;
}
header.metadata_offset = if metadata_len > 0 { card_offset } else { 0 };
header.metadata_len = metadata_len;
header.build_info_offset = if build_info_len > 0 {
card_offset + metadata_len
} else {
0
};
header.build_info_len = build_info_len;
Ok(CardRewritePlan {
header,
copies,
copies_before_card,
card_offset,
new_len: at + MAGIC.len() as u64,
})
}
pub const REWRITE_CHUNK: u64 = 8 << 20;
pub fn rewrite_card<R: RangeReader + ?Sized, W: Write + Seek>(
src: &R,
out: &mut W,
metadata: &[u8],
build_info: &[u8],
progress: &mut dyn FnMut(u64),
) -> Result<Header, FileError> {
rewrite_card_impl(src, out, metadata, build_info, progress, false)
}
fn rewrite_card_impl<R: RangeReader + ?Sized, W: Write + Seek>(
src: &R,
out: &mut W,
metadata: &[u8],
build_info: &[u8],
progress: &mut dyn FnMut(u64),
force_second_pass: bool,
) -> Result<Header, FileError> {
let file_len = src.len();
if file_len < HEADER_LEN as u64 {
return Err(FileError::Container("file shorter than its header"));
}
let head = src.read_at(0, HEADER_LEN as u64)?;
let plan = plan_card_rewrite(
&head,
file_len,
metadata.len() as u64,
build_info.len() as u64,
)?;
let mut header = plan.header.clone();
let parts = content_hash_parts(&header);
let streamable = !force_second_pass && parts.windows(2).all(|w| w[0].0 + w[0].1 <= w[1].0);
let mut hasher = blake3::Hasher::new();
let feed = HashFeed {
parts: &parts,
next: 0,
};
out.write_all(&header.to_bytes())?;
let mut sink = Sink {
out: &mut *out,
written: HEADER_LEN as u64,
streamable,
feed,
hasher: &mut hasher,
};
let mut copied = 0u64;
for i in 0..=plan.copies.len() {
if i == plan.copies_before_card {
sink.write(metadata)?;
sink.write(build_info)?;
}
let Some(&(from, len)) = plan.copies.get(i) else {
break;
};
let mut at = from;
while at < from + len {
let n = (from + len - at).min(REWRITE_CHUNK);
let buf = src.read_at(at, n)?;
if buf.len() as u64 != n {
return Err(FileError::Container("short read while copying"));
}
sink.write(&buf)?;
at += n;
copied += n;
progress(copied);
}
}
sink.write(&MAGIC)?;
let (written, folded) = (sink.written, sink.feed.next);
if written != plan.new_len {
return Err(FileError::Container(
"the rewrite came out the wrong length",
));
}
if !streamable || folded < parts.len() {
hasher = blake3::Hasher::new();
hasher.update(metadata);
let mut old = Header::from_bytes(&head)?;
old.metadata_len = 0;
for (off, len) in content_hash_parts(&old) {
let mut at = off;
while at < off + len {
let n = (off + len - at).min(REWRITE_CHUNK);
hasher.update(&src.read_at(at, n)?);
at += n;
}
}
}
header
.content_hash
.copy_from_slice(&hasher.finalize().as_bytes()[..16]);
out.seek(SeekFrom::Start(0))?;
out.write_all(&header.to_bytes())?;
out.flush()?;
Ok(header)
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct CardAppendPlan {
pub header: Header,
pub card_offset: u64,
pub old_len: u64,
pub dead_bytes: u64,
pub old_head: Vec<u8>,
}
pub fn plan_card_append(
head: &[u8],
file_len: u64,
metadata_len: u64,
in_place: bool,
) -> Result<CardAppendPlan, FileError> {
let mut header = Header::from_bytes(head)?;
let old_len = header.expected_file_len().ok_or(FileError::Container(
"the header's section directory overflows",
))?;
if old_len > file_len {
return Err(FileError::Container(
"the file is shorter than its header records (truncated?)",
));
}
let footer = MAGIC.len() as u64;
let card_offset = if in_place { old_len } else { old_len - footer };
let dead_bytes = header
.metadata_len
.saturating_add(header.build_info_len)
.saturating_add(if in_place { footer } else { 0 });
if metadata_len > 0 {
header.metadata_offset = card_offset;
header.metadata_len = metadata_len;
} else {
header.metadata_offset = 0;
header.metadata_len = 0;
}
header.build_info_offset = 0;
header.build_info_len = 0;
Ok(CardAppendPlan {
header,
card_offset,
old_len,
dead_bytes,
old_head: head[..HEADER_LEN].to_vec(),
})
}
pub fn card_append_hash<R: RangeReader + ?Sized>(
src: &R,
plan: &CardAppendPlan,
metadata: &[u8],
progress: &mut dyn FnMut(u64),
) -> Result<[u8; 16], FileError> {
if metadata.len() as u64 != plan.header.metadata_len {
return Err(FileError::Container(
"the card is not the length the plan was made for",
));
}
let footer = MAGIC.len() as u64;
if src.len() < plan.old_len || src.read_at(plan.old_len - footer, footer)? != MAGIC {
return Err(FileError::Container(
"the file does not end in its footer where its header says",
));
}
let mut hasher = blake3::Hasher::new();
let mut read = 0u64;
for (i, (off, len)) in content_hash_parts(&plan.header).into_iter().enumerate() {
if i == 0 && !metadata.is_empty() {
hasher.update(metadata);
continue;
}
if off
.checked_add(len)
.is_none_or(|end| end > plan.old_len - footer)
{
return Err(FileError::Container("a hashed section overruns the file"));
}
let mut at = off;
while at < off + len {
let n = (off + len - at).min(REWRITE_CHUNK);
let buf = src.read_at(at, n)?;
if buf.len() as u64 != n {
return Err(FileError::Container("short read while hashing"));
}
hasher.update(&buf);
at += n;
read += n;
progress(read);
}
}
let mut hash = [0u8; 16];
hash.copy_from_slice(&hasher.finalize().as_bytes()[..16]);
Ok(hash)
}
#[cfg(not(target_arch = "wasm32"))]
pub fn commit_card_append(
file: &mut std::fs::File,
plan: &CardAppendPlan,
hash: [u8; 16],
metadata: &[u8],
build_info: &[u8],
) -> Result<Header, FileError> {
commit_card_append_impl(file, plan, hash, metadata, build_info, false)
}
#[cfg(not(target_arch = "wasm32"))]
fn commit_card_append_impl(
file: &mut std::fs::File,
plan: &CardAppendPlan,
hash: [u8; 16],
metadata: &[u8],
build_info: &[u8],
fail_before_header: bool,
) -> Result<Header, FileError> {
use std::io::Read;
if metadata.len() as u64 != plan.header.metadata_len {
return Err(FileError::Container(
"the card is not the length the plan was made for",
));
}
let mut head = vec![0u8; HEADER_LEN];
file.seek(SeekFrom::Start(0))?;
file.read_exact(&mut head)?;
if head != plan.old_head || file.metadata()?.len() < plan.old_len {
return Err(FileError::Container(
"the file changed since the card was planned; nothing was written",
));
}
let mut header = plan.header.clone();
header.content_hash = hash;
let info_offset = plan.card_offset + metadata.len() as u64;
if !build_info.is_empty() {
header.build_info_offset = info_offset;
header.build_info_len = build_info.len() as u64;
}
let end = info_offset + build_info.len() as u64 + MAGIC.len() as u64;
debug_assert_eq!(header.expected_file_len(), Some(end));
file.seek(SeekFrom::Start(plan.card_offset))?;
let mut tail = Vec::with_capacity((end - plan.card_offset) as usize);
tail.extend_from_slice(metadata);
tail.extend_from_slice(build_info);
tail.extend_from_slice(&MAGIC);
file.write_all(&tail)?;
file.set_len(end)?;
file.sync_all()?;
if fail_before_header {
return Err(FileError::Container("simulated crash before the header"));
}
file.seek(SeekFrom::Start(0))?;
file.write_all(&header.to_bytes())?;
file.sync_all()?;
Ok(header)
}
struct Sink<'a, 'p, W> {
out: &'a mut W,
written: u64,
streamable: bool,
feed: HashFeed<'p>,
hasher: &'a mut blake3::Hasher,
}
impl<W: Write> Sink<'_, '_, W> {
fn write(&mut self, buf: &[u8]) -> Result<(), FileError> {
self.out.write_all(buf)?;
if self.streamable {
self.feed.fold(self.written, buf, self.hasher);
}
self.written += buf.len() as u64;
Ok(())
}
}
struct HashFeed<'a> {
parts: &'a [(u64, u64)],
next: usize,
}
impl HashFeed<'_> {
fn fold(&mut self, at: u64, buf: &[u8], hasher: &mut blake3::Hasher) {
let end = at + buf.len() as u64;
while let Some(&(off, len)) = self.parts.get(self.next) {
let part_end = off + len;
if off >= end {
return;
}
let lo = off.max(at);
let hi = part_end.min(end);
if lo < hi {
hasher.update(&buf[(lo - at) as usize..(hi - at) as usize]);
}
if part_end <= end {
self.next += 1;
} else {
return;
}
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::card::{derive_card, CardInput};
use crate::reader::SliceReader;
fn q(s: &str, p: &str, o: &str) -> (String, String, String, Option<String>) {
(s.into(), p.into(), o.into(), None)
}
fn quads() -> Vec<(String, String, String, Option<String>)> {
let ty = "<http://www.w3.org/1999/02/22-rdf-syntax-ns#type>";
let mut v = Vec::new();
for i in 0..20 {
let s = format!("<http://ex/item/{i}>");
v.push(q(&s, ty, "<http://ex/Item>"));
v.push(q(
&s,
"<http://ex/next>",
&format!("<http://ex/item/{}>", (i + 1) % 20),
));
v.push(q(&s, "<http://ex/name>", &format!("\"item {i}\"")));
}
v.push((
"<http://ex/a>".into(),
"<http://ex/p>".into(),
"<http://ex/b>".into(),
Some("<http://ex/g>".into()),
));
v
}
fn carded(text_index: bool) -> Vec<u8> {
let quads = quads();
let card = derive_card(&quads, 50, 1, CardInput::default()).to_json_bytes();
let (bytes, _) = crate::ingest::assemble_dataset_with_opts(
quads,
true,
text_index,
None,
move |_, _| card.clone(),
);
crate::format::attach_build_info(&bytes, br#"{"schema":1,"builder":"old"}"#).unwrap()
}
fn rewrite(src: &[u8], meta: &[u8], info: &[u8]) -> Vec<u8> {
let mut out = std::io::Cursor::new(Vec::new());
rewrite_card(&SliceReader::new(src), &mut out, meta, info, &mut |_| {}).unwrap();
out.into_inner()
}
#[test]
fn a_rewrite_copies_every_other_section_and_rehashes() {
for text_index in [false, true] {
let src = carded(text_index);
let old = Header::from_bytes(&src).unwrap();
let meta = br#"{"title":"new card","triple_count":60,"quad_count":61,"named_graph_count":1,"term_count":50,"format_version":5}"#;
let info = br#"{"schema":1,"builder":"recard"}"#;
let out = rewrite(&src, meta, info);
let new = Header::from_bytes(&out).unwrap();
assert!(
crate::format::verify(&out).unwrap(),
"the new hash verifies"
);
assert_ne!(
new.content_hash, old.content_hash,
"the card is inside the hash"
);
let sec = |b: &[u8], off: u64, len: u64| b[off as usize..(off + len) as usize].to_vec();
assert_eq!(
sec(&out, new.metadata_offset, new.metadata_len),
meta.to_vec()
);
assert_eq!(
sec(&out, new.build_info_offset, new.build_info_len),
info.to_vec()
);
for (o, n) in [
(
(old.dictionary_offset, old.dictionary_len),
(new.dictionary_offset, new.dictionary_len),
),
(
(old.root_dir_offset, old.root_dir_len),
(new.root_dir_offset, new.root_dir_len),
),
(
(old.named_graphs_offset, old.named_graphs_len),
(new.named_graphs_offset, new.named_graphs_len),
),
(
(old.text_index_offset, old.text_index_len),
(new.text_index_offset, new.text_index_len),
),
] {
assert_eq!(o.1, n.1);
assert_eq!(sec(&src, o.0, o.1), sec(&out, n.0, n.1));
}
assert_eq!(&out[out.len() - 4..], &src[src.len() - 4..]);
let spliced = crate::format::attach_build_info(
&crate::format::replace_metadata(&src, meta).unwrap(),
info,
)
.unwrap();
assert_eq!(out, spliced);
let rete = crate::Rete::open(&out).unwrap();
assert_eq!(rete.dump(None).len(), 60);
}
}
#[test]
fn the_second_pass_hash_agrees_with_the_streamed_one() {
let src = carded(false);
for meta in [&b"{}"[..], &[b'x'; 5000][..]] {
let streamed = rewrite(&src, meta, b"{}");
let mut out = std::io::Cursor::new(Vec::new());
rewrite_card_impl(
&SliceReader::new(&src),
&mut out,
meta,
b"{}",
&mut |_| {},
true,
)
.unwrap();
assert_eq!(out.into_inner(), streamed);
}
}
#[test]
fn a_rewrite_can_shrink_and_remove() {
let src = carded(false);
let out = rewrite(&src, b"{}", b"");
let h = Header::from_bytes(&out).unwrap();
assert_eq!(h.build_info_len, 0);
assert!(crate::format::verify(&out).unwrap());
let out = rewrite(&src, b"", b"");
let h = Header::from_bytes(&out).unwrap();
assert_eq!((h.metadata_len, h.build_info_len), (0, 0));
assert!(crate::format::verify(&out).unwrap());
assert!(crate::Rete::open(&out).is_ok());
}
#[test]
fn a_cardless_file_gains_a_card_after_the_header() {
let (src, _) =
crate::ingest::assemble_dataset_with_opts(quads(), true, false, None, |_, _| {
Vec::new()
});
let out = rewrite(&src, br#"{"title":"t"}"#, br#"{"schema":1}"#);
let h = Header::from_bytes(&out).unwrap();
assert_eq!(h.metadata_offset, HEADER_LEN as u64);
assert_eq!(h.build_info_offset, HEADER_LEN as u64 + h.metadata_len);
assert!(crate::format::verify(&out).unwrap());
}
#[test]
fn a_detached_build_record_is_refused() {
let src = carded(false);
let mut h = Header::from_bytes(&src).unwrap();
h.build_info_offset += 1;
let err = plan_card_rewrite(&h.to_bytes(), src.len() as u64, 10, 10).unwrap_err();
assert!(err.to_string().contains("not adjacent"), "{err}");
}
struct Scratch(std::path::PathBuf);
impl Scratch {
fn new(tag: &str, bytes: &[u8]) -> Self {
use std::sync::atomic::{AtomicU64, Ordering};
static N: AtomicU64 = AtomicU64::new(0);
let path = std::env::temp_dir().join(format!(
"rete-recard-{tag}-{}-{}.rete",
std::process::id(),
N.fetch_add(1, Ordering::Relaxed)
));
std::fs::write(&path, bytes).unwrap();
Scratch(path)
}
fn bytes(&self) -> Vec<u8> {
std::fs::read(&self.0).unwrap()
}
fn open(&self) -> std::fs::File {
std::fs::OpenOptions::new()
.read(true)
.write(true)
.open(&self.0)
.unwrap()
}
}
impl Drop for Scratch {
fn drop(&mut self) {
let _ = std::fs::remove_file(&self.0);
}
}
fn append(file: &Scratch, meta: &[u8], info: &[u8], in_place: bool) -> Header {
let bytes = file.bytes();
let plan =
plan_card_append(&bytes, bytes.len() as u64, meta.len() as u64, in_place).unwrap();
let hash = card_append_hash(&SliceReader::new(&bytes), &plan, meta, &mut |_| {}).unwrap();
commit_card_append(&mut file.open(), &plan, hash, meta, info).unwrap()
}
const META: &[u8] = br#"{"title":"after the data","triple_count":60,"quad_count":61,"named_graph_count":1,"term_count":50,"format_version":5}"#;
const INFO: &[u8] = br#"{"schema":1,"builder":"in place"}"#;
#[test]
fn a_card_appended_in_place_moves_nothing_and_verifies() {
for text_index in [false, true] {
let src = carded(text_index);
let old = Header::from_bytes(&src).unwrap();
let file = Scratch::new("inplace", &src);
let new = append(&file, META, INFO, true);
let out = file.bytes();
assert_eq!(
new.metadata_offset,
src.len() as u64,
"after the old footer"
);
assert_eq!(
new.build_info_offset,
new.metadata_offset + META.len() as u64
);
assert_eq!(new.expected_file_len(), Some(out.len() as u64));
assert_eq!(&out[out.len() - 4..], &MAGIC);
assert_eq!(
&out[..],
&{
let mut want = src[..].to_vec();
want[..HEADER_LEN].copy_from_slice(&new.to_bytes());
want.extend_from_slice(META);
want.extend_from_slice(INFO);
want.extend_from_slice(&MAGIC);
want
}[..]
);
for (o, n) in [
(old.dictionary_offset, new.dictionary_offset),
(old.root_dir_offset, new.root_dir_offset),
(old.named_graphs_offset, new.named_graphs_offset),
(old.text_index_offset, new.text_index_offset),
] {
assert_eq!(o, n, "a data section moved");
}
assert!(crate::format::verify(&out).unwrap());
let copied = rewrite(&src, META, INFO);
assert_eq!(
new.content_hash,
Header::from_bytes(&copied).unwrap().content_hash,
"the hash does not depend on where the card lies"
);
let (m, b) =
crate::file::read_card_and_build_info_ranged(&SliceReader::new(&out)).unwrap();
assert_eq!((m.unwrap(), b.unwrap()), (META.to_vec(), INFO.to_vec()));
let rete = crate::Rete::open(&out).unwrap();
assert_eq!(rete.metadata(), Some(META));
assert_eq!(rete.dump(None).len(), 60);
}
}
#[test]
fn a_crash_before_the_header_leaves_the_old_file() {
let src = carded(false);
let file = Scratch::new("crash", &src);
let plan = plan_card_append(&src, src.len() as u64, META.len() as u64, true).unwrap();
let hash = card_append_hash(&SliceReader::new(&src), &plan, META, &mut |_| {}).unwrap();
let err =
commit_card_append_impl(&mut file.open(), &plan, hash, META, INFO, true).unwrap_err();
assert!(err.to_string().contains("simulated"), "{err}");
let crashed = file.bytes();
assert!(crashed.len() > src.len(), "the new sections were written");
assert_eq!(&crashed[..src.len()], &src[..], "the old file is untouched");
assert!(crate::format::verify(&crashed).unwrap());
let rete = crate::Rete::open(&crashed).unwrap();
assert_eq!(
rete.metadata(),
crate::Rete::open(&src).unwrap().metadata(),
"the old card"
);
assert_eq!(rete.dump(None).len(), 60);
let rerun = append(&file, META, INFO, true);
let clean = Scratch::new("clean", &src);
append(&clean, META, INFO, true);
assert_eq!(file.bytes(), clean.bytes());
assert!(crate::format::verify(&file.bytes()).unwrap());
assert_eq!(rerun.expected_file_len(), Some(file.bytes().len() as u64));
}
#[test]
fn a_fresh_append_replaces_the_footer() {
let (src, _) =
crate::ingest::assemble_dataset_with_opts(quads(), true, false, None, |_, _| {
Vec::new()
});
let file = Scratch::new("fresh", &src);
let plan = plan_card_append(&src, src.len() as u64, META.len() as u64, false).unwrap();
assert_eq!(plan.dead_bytes, 0);
let h = append(&file, META, INFO, false);
let out = file.bytes();
assert_eq!(h.metadata_offset, src.len() as u64 - 4);
assert_eq!(out.len(), src.len() + META.len() + INFO.len());
assert!(crate::format::verify(&out).unwrap());
assert_eq!(
Header::from_bytes(&rewrite(&src, META, INFO))
.unwrap()
.content_hash,
h.content_hash
);
}
#[test]
fn a_card_after_the_data_can_be_replaced_again_every_way() {
let src = carded(true);
let file = Scratch::new("again", &src);
append(&file, META, INFO, true);
let once = file.bytes();
let plan = plan_card_append(&once, once.len() as u64, 2, true).unwrap();
assert_eq!(
plan.dead_bytes,
(META.len() + INFO.len() + 4) as u64,
"the first in-place card, its record and the footer before it"
);
append(&file, b"{}", b"{\"schema\":1}", true);
assert!(crate::format::verify(&file.bytes()).unwrap());
let meta2 = br#"{"title":"copied"}"#;
let copied = rewrite(&once, meta2, INFO);
let h = Header::from_bytes(&copied).unwrap();
assert!(h.metadata_offset > h.dictionary_offset);
let old = Header::from_bytes(&src).unwrap();
assert_eq!(
copied.len() as u64,
src.len() as u64 - old.metadata_len - old.build_info_len
+ (meta2.len() + INFO.len()) as u64
);
assert_eq!(h.expected_file_len(), Some(copied.len() as u64));
assert!(crate::format::verify(&copied).unwrap());
assert_eq!(crate::Rete::open(&copied).unwrap().dump(None).len(), 60);
assert_eq!(h.dictionary_offset, HEADER_LEN as u64);
let sec = |b: &[u8], off: u64, len: u64| b[off as usize..(off + len) as usize].to_vec();
assert_eq!(
sec(&copied, h.root_dir_offset, h.root_dir_len),
sec(&src, old.root_dir_offset, old.root_dir_len)
);
let swapped = crate::format::attach_build_info(&once, b"{\"schema\":1,\"x\":2}").unwrap();
let h = Header::from_bytes(&swapped).unwrap();
assert_eq!(h.build_info_offset, h.metadata_offset + h.metadata_len);
assert_eq!(h.expected_file_len(), Some(swapped.len() as u64));
assert!(crate::format::verify(&swapped).unwrap());
}
#[test]
fn an_append_refuses_what_it_cannot_vouch_for() {
let src = carded(false);
let err = plan_card_append(&src, src.len() as u64 - 1, 10, true).unwrap_err();
assert!(err.to_string().contains("shorter"), "{err}");
let mut broken = src.clone();
let n = broken.len();
broken[n - 1] = b'X';
let plan = plan_card_append(&broken, n as u64, META.len() as u64, true).unwrap();
let err =
card_append_hash(&SliceReader::new(&broken), &plan, META, &mut |_| {}).unwrap_err();
assert!(err.to_string().contains("footer"), "{err}");
let file = Scratch::new("changed", &src);
let plan = plan_card_append(&src, src.len() as u64, META.len() as u64, true).unwrap();
let hash = card_append_hash(&SliceReader::new(&src), &plan, META, &mut |_| {}).unwrap();
append(&file, b"{}", b"", true); let before = file.bytes();
let err = commit_card_append(&mut file.open(), &plan, hash, META, INFO).unwrap_err();
assert!(err.to_string().contains("changed"), "{err}");
assert_eq!(file.bytes(), before, "nothing was written");
}
#[test]
fn the_hash_feed_folds_parts_across_write_boundaries() {
let data: Vec<u8> = (0..200u8).collect();
let parts = [(10u64, 30u64), (40, 5), (100, 50)];
let mut want = blake3::Hasher::new();
for &(o, l) in &parts {
want.update(&data[o as usize..(o + l) as usize]);
}
let mut got = blake3::Hasher::new();
let mut feed = HashFeed {
parts: &parts,
next: 0,
};
for (i, chunk) in data.chunks(7).enumerate() {
feed.fold(i as u64 * 7, chunk, &mut got);
}
assert_eq!(want.finalize(), got.finalize());
assert_eq!(feed.next, parts.len());
}
}