1use std::io::{Seek, SeekFrom, Write};
26
27use crate::file::{content_hash_parts, FileError};
28use crate::header::{Header, HEADER_LEN};
29use crate::reader::RangeReader;
30
31#[derive(Debug, Clone, PartialEq, Eq)]
33pub struct CardRewritePlan {
34 pub header: Header,
38 pub before: (u64, u64),
42 pub after: (u64, u64),
45 pub card_offset: u64,
47 pub new_len: u64,
49}
50
51pub fn plan_card_rewrite(
63 head: &[u8],
64 file_len: u64,
65 metadata_len: u64,
66 build_info_len: u64,
67) -> Result<CardRewritePlan, FileError> {
68 let mut header = Header::from_bytes(head)?;
69 let (m_off, m_len) = (header.metadata_offset, header.metadata_len);
70 let (b_off, b_len) = (header.build_info_offset, header.build_info_len);
71 let (start, end) = match (m_len > 0, b_len > 0) {
72 (true, true) => {
73 if b_off != m_off.saturating_add(m_len) {
74 return Err(FileError::Container(
75 "the build-info section is not adjacent to the metadata section",
76 ));
77 }
78 (m_off, b_off.saturating_add(b_len))
79 }
80 (true, false) => (m_off, m_off.saturating_add(m_len)),
81 (false, true) => (b_off, b_off.saturating_add(b_len)),
82 (false, false) => (HEADER_LEN as u64, HEADER_LEN as u64),
83 };
84 if start < HEADER_LEN as u64 || end > file_len {
85 return Err(FileError::Container(
86 "the card section lies outside the file",
87 ));
88 }
89 let new_span = metadata_len.saturating_add(build_info_len);
90 let shift = |off: u64| -> u64 { off - (end - start) + new_span };
91
92 let check = |off: &mut u64, len: u64| -> Result<(), FileError> {
94 if len == 0 {
95 return Ok(());
96 }
97 let sec_end = off.saturating_add(len);
98 if sec_end > file_len {
99 return Err(FileError::Container("a section overruns the file"));
100 }
101 if sec_end <= start {
102 Ok(())
103 } else if *off >= end {
104 *off = shift(*off);
105 Ok(())
106 } else {
107 Err(FileError::Container(
108 "a section overlaps the card; this layout cannot be re-carded in place",
109 ))
110 }
111 };
112 check(&mut header.dictionary_offset, header.dictionary_len)?;
113 check(&mut header.root_dir_offset, header.root_dir_len)?;
114 check(&mut header.pyramid_meta_offset, header.pyramid_meta_len)?;
115 check(&mut header.named_graphs_offset, header.named_graphs_len)?;
116 check(&mut header.text_index_offset, header.text_index_len)?;
117 for s in &mut header.extra_sections {
118 check(&mut s.offset, s.length)?;
119 }
120
121 header.metadata_offset = if metadata_len > 0 { start } else { 0 };
122 header.metadata_len = metadata_len;
123 header.build_info_offset = if build_info_len > 0 {
124 start + metadata_len
125 } else {
126 0
127 };
128 header.build_info_len = build_info_len;
129 Ok(CardRewritePlan {
130 header,
131 before: (HEADER_LEN as u64, start),
132 after: (end, file_len),
133 card_offset: start,
134 new_len: file_len - (end - start) + new_span,
135 })
136}
137
138pub const REWRITE_CHUNK: u64 = 8 << 20;
141
142pub fn rewrite_card<R: RangeReader + ?Sized, W: Write + Seek>(
155 src: &R,
156 out: &mut W,
157 metadata: &[u8],
158 build_info: &[u8],
159 progress: &mut dyn FnMut(u64),
160) -> Result<Header, FileError> {
161 rewrite_card_impl(src, out, metadata, build_info, progress, false)
162}
163
164fn rewrite_card_impl<R: RangeReader + ?Sized, W: Write + Seek>(
167 src: &R,
168 out: &mut W,
169 metadata: &[u8],
170 build_info: &[u8],
171 progress: &mut dyn FnMut(u64),
172 force_second_pass: bool,
173) -> Result<Header, FileError> {
174 let file_len = src.len();
175 if file_len < HEADER_LEN as u64 {
176 return Err(FileError::Container("file shorter than its header"));
177 }
178 let head = src.read_at(0, HEADER_LEN as u64)?;
179 let plan = plan_card_rewrite(
180 &head,
181 file_len,
182 metadata.len() as u64,
183 build_info.len() as u64,
184 )?;
185 let mut header = plan.header.clone();
186
187 let parts = content_hash_parts(&header);
190 let streamable = !force_second_pass
191 && parts.windows(2).all(|w| w[0].0 + w[0].1 <= w[1].0)
192 && parts
193 .iter()
194 .all(|&(off, len)| off >= plan.card_offset || off + len <= plan.card_offset);
195 let mut hasher = blake3::Hasher::new();
196 let feed = HashFeed {
197 parts: &parts,
198 next: 0,
199 };
200
201 out.write_all(&header.to_bytes())?;
202 let mut sink = Sink {
203 out: &mut *out,
204 written: HEADER_LEN as u64,
205 streamable,
206 feed,
207 hasher: &mut hasher,
208 };
209 let mut copied = 0u64;
210 for (from, to, then) in [
211 (plan.before.0, plan.before.1, Some([metadata, build_info])),
212 (plan.after.0, plan.after.1, None),
213 ] {
214 let mut at = from;
215 while at < to {
216 let n = (to - at).min(REWRITE_CHUNK);
217 let buf = src.read_at(at, n)?;
218 if buf.len() as u64 != n {
219 return Err(FileError::Container("short read while copying"));
220 }
221 sink.write(&buf)?;
222 at += n;
223 copied += n;
224 progress(copied);
225 }
226 for section in then.into_iter().flatten() {
227 sink.write(section)?;
228 }
229 }
230 let (written, folded) = (sink.written, sink.feed.next);
231 if written != plan.new_len {
232 return Err(FileError::Container(
233 "the rewrite came out the wrong length",
234 ));
235 }
236
237 if !streamable || folded < parts.len() {
238 hasher = blake3::Hasher::new();
241 let delta = |off: u64| -> u64 {
244 if off >= plan.card_offset + metadata.len() as u64 + build_info.len() as u64 {
245 off + file_len - plan.new_len
246 } else {
247 off
248 }
249 };
250 for &(off, len) in &parts {
251 if metadata.len() as u64 == len && off == plan.card_offset && !metadata.is_empty() {
252 hasher.update(metadata);
253 continue;
254 }
255 let mut at = delta(off);
256 let end = at + len;
257 while at < end {
258 let n = (end - at).min(REWRITE_CHUNK);
259 hasher.update(&src.read_at(at, n)?);
260 at += n;
261 }
262 }
263 }
264 header
265 .content_hash
266 .copy_from_slice(&hasher.finalize().as_bytes()[..16]);
267 out.seek(SeekFrom::Start(0))?;
268 out.write_all(&header.to_bytes())?;
269 out.flush()?;
270 Ok(header)
271}
272
273struct Sink<'a, 'p, W> {
276 out: &'a mut W,
277 written: u64,
278 streamable: bool,
279 feed: HashFeed<'p>,
280 hasher: &'a mut blake3::Hasher,
281}
282
283impl<W: Write> Sink<'_, '_, W> {
284 fn write(&mut self, buf: &[u8]) -> Result<(), FileError> {
285 self.out.write_all(buf)?;
286 if self.streamable {
287 self.feed.fold(self.written, buf, self.hasher);
288 }
289 self.written += buf.len() as u64;
290 Ok(())
291 }
292}
293
294struct HashFeed<'a> {
297 parts: &'a [(u64, u64)],
298 next: usize,
299}
300
301impl HashFeed<'_> {
302 fn fold(&mut self, at: u64, buf: &[u8], hasher: &mut blake3::Hasher) {
303 let end = at + buf.len() as u64;
304 while let Some(&(off, len)) = self.parts.get(self.next) {
305 let part_end = off + len;
306 if off >= end {
307 return;
308 }
309 let lo = off.max(at);
310 let hi = part_end.min(end);
311 if lo < hi {
312 hasher.update(&buf[(lo - at) as usize..(hi - at) as usize]);
313 }
314 if part_end <= end {
315 self.next += 1;
316 } else {
317 return;
318 }
319 }
320 }
321}
322
323#[cfg(test)]
324mod tests {
325 use super::*;
326 use crate::card::{derive_card, CardInput};
327 use crate::reader::SliceReader;
328
329 fn q(s: &str, p: &str, o: &str) -> (String, String, String, Option<String>) {
330 (s.into(), p.into(), o.into(), None)
331 }
332
333 fn quads() -> Vec<(String, String, String, Option<String>)> {
334 let ty = "<http://www.w3.org/1999/02/22-rdf-syntax-ns#type>";
335 let mut v = Vec::new();
336 for i in 0..20 {
337 let s = format!("<http://ex/item/{i}>");
338 v.push(q(&s, ty, "<http://ex/Item>"));
339 v.push(q(
340 &s,
341 "<http://ex/next>",
342 &format!("<http://ex/item/{}>", (i + 1) % 20),
343 ));
344 v.push(q(&s, "<http://ex/name>", &format!("\"item {i}\"")));
345 }
346 v.push((
347 "<http://ex/a>".into(),
348 "<http://ex/p>".into(),
349 "<http://ex/b>".into(),
350 Some("<http://ex/g>".into()),
351 ));
352 v
353 }
354
355 fn carded(text_index: bool) -> Vec<u8> {
358 let quads = quads();
359 let card = derive_card(&quads, 50, 1, CardInput::default()).to_json_bytes();
360 let (bytes, _) = crate::ingest::assemble_dataset_with_opts(
361 quads,
362 true,
363 text_index,
364 None,
365 move |_, _| card.clone(),
366 );
367 crate::format::attach_build_info(&bytes, br#"{"schema":1,"builder":"old"}"#).unwrap()
368 }
369
370 fn rewrite(src: &[u8], meta: &[u8], info: &[u8]) -> Vec<u8> {
371 let mut out = std::io::Cursor::new(Vec::new());
372 rewrite_card(&SliceReader::new(src), &mut out, meta, info, &mut |_| {}).unwrap();
373 out.into_inner()
374 }
375
376 #[test]
380 fn a_rewrite_copies_every_other_section_and_rehashes() {
381 for text_index in [false, true] {
382 let src = carded(text_index);
383 let old = Header::from_bytes(&src).unwrap();
384 let meta = br#"{"title":"new card","triple_count":60,"quad_count":61,"named_graph_count":1,"term_count":50,"format_version":5}"#;
385 let info = br#"{"schema":1,"builder":"recard"}"#;
386 let out = rewrite(&src, meta, info);
387 let new = Header::from_bytes(&out).unwrap();
388 assert!(
389 crate::format::verify(&out).unwrap(),
390 "the new hash verifies"
391 );
392 assert_ne!(
393 new.content_hash, old.content_hash,
394 "the card is inside the hash"
395 );
396 let sec = |b: &[u8], off: u64, len: u64| b[off as usize..(off + len) as usize].to_vec();
397 assert_eq!(
398 sec(&out, new.metadata_offset, new.metadata_len),
399 meta.to_vec()
400 );
401 assert_eq!(
402 sec(&out, new.build_info_offset, new.build_info_len),
403 info.to_vec()
404 );
405 for (o, n) in [
406 (
407 (old.dictionary_offset, old.dictionary_len),
408 (new.dictionary_offset, new.dictionary_len),
409 ),
410 (
411 (old.root_dir_offset, old.root_dir_len),
412 (new.root_dir_offset, new.root_dir_len),
413 ),
414 (
415 (old.named_graphs_offset, old.named_graphs_len),
416 (new.named_graphs_offset, new.named_graphs_len),
417 ),
418 (
419 (old.text_index_offset, old.text_index_len),
420 (new.text_index_offset, new.text_index_len),
421 ),
422 ] {
423 assert_eq!(o.1, n.1);
424 assert_eq!(sec(&src, o.0, o.1), sec(&out, n.0, n.1));
425 }
426 assert_eq!(&out[out.len() - 4..], &src[src.len() - 4..]);
428 let spliced = crate::format::attach_build_info(
431 &crate::format::replace_metadata(&src, meta).unwrap(),
432 info,
433 )
434 .unwrap();
435 assert_eq!(out, spliced);
436 let rete = crate::Rete::open(&out).unwrap();
438 assert_eq!(rete.dump(None).len(), 60);
439 }
440 }
441
442 #[test]
446 fn the_second_pass_hash_agrees_with_the_streamed_one() {
447 let src = carded(false);
448 for meta in [&b"{}"[..], &[b'x'; 5000][..]] {
449 let streamed = rewrite(&src, meta, b"{}");
450 let mut out = std::io::Cursor::new(Vec::new());
451 rewrite_card_impl(
452 &SliceReader::new(&src),
453 &mut out,
454 meta,
455 b"{}",
456 &mut |_| {},
457 true,
458 )
459 .unwrap();
460 assert_eq!(out.into_inner(), streamed);
461 }
462 }
463
464 #[test]
467 fn a_rewrite_can_shrink_and_remove() {
468 let src = carded(false);
469 let out = rewrite(&src, b"{}", b"");
470 let h = Header::from_bytes(&out).unwrap();
471 assert_eq!(h.build_info_len, 0);
472 assert!(crate::format::verify(&out).unwrap());
473 let out = rewrite(&src, b"", b"");
474 let h = Header::from_bytes(&out).unwrap();
475 assert_eq!((h.metadata_len, h.build_info_len), (0, 0));
476 assert!(crate::format::verify(&out).unwrap());
477 assert!(crate::Rete::open(&out).is_ok());
478 }
479
480 #[test]
482 fn a_cardless_file_gains_a_card_after_the_header() {
483 let (src, _) =
484 crate::ingest::assemble_dataset_with_opts(quads(), true, false, None, |_, _| {
485 Vec::new()
486 });
487 let out = rewrite(&src, br#"{"title":"t"}"#, br#"{"schema":1}"#);
488 let h = Header::from_bytes(&out).unwrap();
489 assert_eq!(h.metadata_offset, HEADER_LEN as u64);
490 assert_eq!(h.build_info_offset, HEADER_LEN as u64 + h.metadata_len);
491 assert!(crate::format::verify(&out).unwrap());
492 }
493
494 #[test]
497 fn a_detached_build_record_is_refused() {
498 let src = carded(false);
499 let mut h = Header::from_bytes(&src).unwrap();
500 h.build_info_offset += 1;
501 let err = plan_card_rewrite(&h.to_bytes(), src.len() as u64, 10, 10).unwrap_err();
502 assert!(err.to_string().contains("not adjacent"), "{err}");
503 }
504
505 #[test]
508 fn the_hash_feed_folds_parts_across_write_boundaries() {
509 let data: Vec<u8> = (0..200u8).collect();
510 let parts = [(10u64, 30u64), (40, 5), (100, 50)];
511 let mut want = blake3::Hasher::new();
512 for &(o, l) in &parts {
513 want.update(&data[o as usize..(o + l) as usize]);
514 }
515 let mut got = blake3::Hasher::new();
516 let mut feed = HashFeed {
517 parts: &parts,
518 next: 0,
519 };
520 for (i, chunk) in data.chunks(7).enumerate() {
521 feed.fold(i as u64 * 7, chunk, &mut got);
522 }
523 assert_eq!(want.finalize(), got.finalize());
524 assert_eq!(feed.next, parts.len());
525 }
526}