1use std::io::{self, Write};
22
23use crate::bundle::INVALID_ID;
24use crate::customize::Metric;
25use crate::internal::bitvec::BitVector;
26use crate::internal::id_map::LocalIDMapper;
27use crate::structure::Cch;
28
29const STRUCT_MAGIC: u64 = 0x4343_485F_5354_5243; const METRIC_MAGIC: u64 = 0x4343_485F_4D45_5452; const FORMAT_VERSION: u32 = 1;
32
33fn write_u32<W: Write>(out: &mut W, value: u32) -> io::Result<()> {
35 out.write_all(&value.to_le_bytes())
36}
37
38fn write_u64<W: Write>(out: &mut W, value: u64) -> io::Result<()> {
40 out.write_all(&value.to_le_bytes())
41}
42
43fn write_sized_vector<W: Write>(out: &mut W, v: &[u32]) -> io::Result<()> {
46 let byte_length = (v.len() as u64) * 4;
47 write_u64(out, byte_length)?;
48 let mut bytes = Vec::with_capacity(v.len() * 4);
52 for &x in v {
53 bytes.extend_from_slice(&x.to_le_bytes());
54 }
55 out.write_all(&bytes)
56}
57
58fn write_sized_bit_vector<W: Write>(out: &mut W, bv: &BitVector) -> io::Result<()> {
62 let bit_count = bv.len();
63 let byte_length = bit_count.div_ceil(512) * 64;
64 write_u64(out, bit_count)?;
65 write_u64(out, byte_length)?;
66 let word_count = (byte_length / 8) as usize;
70 let mut bytes = Vec::with_capacity(word_count * 8);
71 let words = bv.words();
72 for &w in words {
73 bytes.extend_from_slice(&w.to_le_bytes());
74 }
75 for _ in words.len()..word_count {
76 bytes.extend_from_slice(&0u64.to_le_bytes());
77 }
78 out.write_all(&bytes)
79}
80
81struct OnDiskMapping {
84 is_input_arc_upward: BitVector,
85 does_cch_arc_have_input_arc: BitVector,
86 does_cch_arc_have_extra_input_arc: BitVector,
87 forward_input_arc_of_cch: Vec<u32>,
89 backward_input_arc_of_cch: Vec<u32>,
91 first_extra_forward_input_arc_of_cch: Vec<u32>,
93 first_extra_backward_input_arc_of_cch: Vec<u32>,
94 extra_forward_input_arc_of_cch: Vec<u32>,
96 extra_backward_input_arc_of_cch: Vec<u32>,
97}
98
99#[allow(clippy::cast_possible_truncation)] fn reconstruct_on_disk_mapping(cch: &Cch) -> OnDiskMapping {
104 let input_arc_count = cch.input_arc_to_cch_arc.len();
105 let cch_arc_count = cch.cch_arc_count();
106
107 let mut is_input_arc_upward = BitVector::new(input_arc_count as u64);
112 for &ia in &cch.forward_input_arc_of_cch {
113 if ia != INVALID_ID {
114 is_input_arc_upward.set(u64::from(ia));
115 }
116 }
117 for &ia in &cch.extra_forward_input_arc_of_cch {
118 is_input_arc_upward.set(u64::from(ia));
119 }
120
121 let mut does_cch_arc_have_input_arc = BitVector::new(cch_arc_count as u64);
124 for cch_arc in 0..cch_arc_count {
125 if cch.forward_input_arc_of_cch[cch_arc] != INVALID_ID
126 || cch.backward_input_arc_of_cch[cch_arc] != INVALID_ID
127 {
128 does_cch_arc_have_input_arc.set(cch_arc as u64);
129 }
130 }
131
132 let mut does_cch_arc_have_extra_input_arc = BitVector::new(cch_arc_count as u64);
135 let ff = &cch.first_extra_forward_input_arc_of_cch;
136 let fb = &cch.first_extra_backward_input_arc_of_cch;
137 for cch_arc in 0..cch_arc_count {
138 if ff[cch_arc + 1] > ff[cch_arc] || fb[cch_arc + 1] > fb[cch_arc] {
139 does_cch_arc_have_extra_input_arc.set(cch_arc as u64);
140 }
141 }
142
143 let in_mapper = LocalIDMapper::new(
146 does_cch_arc_have_input_arc.words(),
147 does_cch_arc_have_input_arc.len(),
148 );
149 let local_in = in_mapper.local_id_count() as usize;
150 let mut forward_local = vec![INVALID_ID; local_in];
151 let mut backward_local = vec![INVALID_ID; local_in];
152 for cch_arc in 0..cch_arc_count {
153 if does_cch_arc_have_input_arc.is_set(cch_arc as u64) {
154 let li = in_mapper.to_local(cch_arc as u64) as usize;
155 forward_local[li] = cch.forward_input_arc_of_cch[cch_arc];
156 backward_local[li] = cch.backward_input_arc_of_cch[cch_arc];
157 }
158 }
159
160 let extra_mapper = LocalIDMapper::new(
165 does_cch_arc_have_extra_input_arc.words(),
166 does_cch_arc_have_extra_input_arc.len(),
167 );
168 let local_extra = extra_mapper.local_id_count() as usize;
169
170 let mut first_extra_forward = vec![0u32; local_extra + 1];
171 let mut first_extra_backward = vec![0u32; local_extra + 1];
172 for cch_arc in 0..cch_arc_count {
173 if does_cch_arc_have_extra_input_arc.is_set(cch_arc as u64) {
174 let li = extra_mapper.to_local(cch_arc as u64) as usize;
175 first_extra_forward[li + 1] = ff[cch_arc + 1] - ff[cch_arc];
176 first_extra_backward[li + 1] = fb[cch_arc + 1] - fb[cch_arc];
177 }
178 }
179 for i in 0..local_extra {
180 first_extra_forward[i + 1] += first_extra_forward[i];
181 first_extra_backward[i + 1] += first_extra_backward[i];
182 }
183
184 OnDiskMapping {
185 is_input_arc_upward,
186 does_cch_arc_have_input_arc,
187 does_cch_arc_have_extra_input_arc,
188 forward_input_arc_of_cch: forward_local,
189 backward_input_arc_of_cch: backward_local,
190 first_extra_forward_input_arc_of_cch: first_extra_forward,
191 first_extra_backward_input_arc_of_cch: first_extra_backward,
192 extra_forward_input_arc_of_cch: cch.extra_forward_input_arc_of_cch.clone(),
193 extra_backward_input_arc_of_cch: cch.extra_backward_input_arc_of_cch.clone(),
194 }
195}
196
197struct StructReader<'a> {
202 bytes: &'a [u8],
203 pos: usize,
204}
205
206impl<'a> StructReader<'a> {
207 fn new(bytes: &'a [u8]) -> Self {
208 Self { bytes, pos: 0 }
209 }
210
211 fn read_u64(&mut self) -> io::Result<u64> {
213 let end = self.pos.saturating_add(8);
218 if end > self.bytes.len() {
219 return Err(truncated_err("u64 field"));
220 }
221 let mut buf = [0u8; 8];
222 buf.copy_from_slice(&self.bytes[self.pos..end]);
223 self.pos = end;
224 Ok(u64::from_le_bytes(buf))
225 }
226
227 #[allow(clippy::cast_possible_truncation)] fn read_u64_usize(&mut self) -> io::Result<usize> {
234 Ok(self.read_u64()? as usize)
235 }
236
237 fn read_u32(&mut self) -> io::Result<u32> {
239 let end = self.pos.saturating_add(4);
240 if end > self.bytes.len() {
241 return Err(truncated_err("u32 field"));
242 }
243 let mut buf = [0u8; 4];
244 buf.copy_from_slice(&self.bytes[self.pos..end]);
245 self.pos = end;
246 Ok(u32::from_le_bytes(buf))
247 }
248
249 fn read_sized_vector(&mut self, label: &str, expected_count: usize) -> io::Result<Vec<u32>> {
254 self.read_sized_vector_inner(label, Some(expected_count))
255 }
256
257 fn read_sized_vector_any(&mut self, label: &str) -> io::Result<Vec<u32>> {
261 self.read_sized_vector_inner(label, None)
262 }
263
264 fn read_sized_vector_inner(
265 &mut self,
266 label: &str,
267 expected_count: Option<usize>,
268 ) -> io::Result<Vec<u32>> {
269 let byte_length = self.read_u64_usize()?;
270 if byte_length % 4 != 0 {
271 return Err(io::Error::new(
272 io::ErrorKind::InvalidData,
273 format!("section '{label}' byte_length {byte_length} is not a multiple of 4"),
274 ));
275 }
276 let count = byte_length / 4;
277 if let Some(expected) = expected_count {
278 if expected.checked_mul(4) != Some(byte_length) {
282 return Err(io::Error::new(
283 io::ErrorKind::InvalidData,
284 format!(
285 "section '{label}' byte_length {byte_length} does not match expected \
286 4 * {expected}; header count and section length disagree"
287 ),
288 ));
289 }
290 }
291 let end = self.pos.saturating_add(byte_length);
292 if end > self.bytes.len() {
293 return Err(truncated_err(label));
294 }
295 let mut v = Vec::with_capacity(count);
296 let mut off = self.pos;
297 for _ in 0..count {
298 let mut buf = [0u8; 4];
299 buf.copy_from_slice(&self.bytes[off..off + 4]);
300 v.push(u32::from_le_bytes(buf));
301 off += 4;
302 }
303 self.pos = end;
304 Ok(v)
305 }
306
307 #[allow(clippy::cast_possible_truncation)] fn read_sized_bit_vector(&mut self, label: &str, expected_bits: u64) -> io::Result<BitVector> {
313 let bit_count = self.read_u64()?;
314 if bit_count != expected_bits {
315 return Err(io::Error::new(
316 io::ErrorKind::InvalidData,
317 format!(
318 "section '{label}' bit_count {bit_count} does not match expected \
319 {expected_bits}"
320 ),
321 ));
322 }
323 let byte_length = self.read_u64_usize()?;
324 let expected_byte_length = (byte_length_for_bits(bit_count)).unwrap_or(usize::MAX);
327 if byte_length != expected_byte_length {
328 return Err(io::Error::new(
329 io::ErrorKind::InvalidData,
330 format!(
331 "section '{label}' bitvector byte_length {byte_length} does not match \
332 expected {expected_byte_length} for bit_count {bit_count}"
333 ),
334 ));
335 }
336 let end = self.pos.saturating_add(byte_length);
337 if end > self.bytes.len() {
338 return Err(truncated_err(label));
339 }
340 let word_count = (bit_count.div_ceil(64)) as usize;
346 let mut bv = BitVector::new(bit_count);
347 let mut off = self.pos;
348 for word_idx in 0..word_count {
349 let mut buf = [0u8; 8];
350 buf.copy_from_slice(&self.bytes[off..off + 8]);
351 let word = u64::from_le_bytes(buf);
352 let base = (word_idx as u64) * 64;
355 let mut w = word;
356 while w != 0 {
357 let b = u64::from(w.trailing_zeros());
358 let global = base + b;
359 if global < bit_count {
360 bv.set(global);
361 }
362 w &= w - 1;
363 }
364 off += 8;
365 }
366 self.pos = end;
367 Ok(bv)
368 }
369}
370
371fn truncated_err(label: &str) -> io::Error {
373 io::Error::new(
374 io::ErrorKind::InvalidData,
375 format!("truncated .cch-struct at section '{label}'"),
376 )
377}
378
379fn byte_length_for_bits(bit_count: u64) -> Option<usize> {
382 usize::try_from(bit_count.div_ceil(512))
383 .ok()
384 .and_then(|blocks| blocks.checked_mul(64))
385}
386
387#[allow(clippy::cast_possible_truncation)] fn expand_local_to_full(
395 presence: &BitVector,
396 mapper: &LocalIDMapper,
397 local: &[u32],
398 cch_arc_count: usize,
399) -> Vec<u32> {
400 let mut full = vec![INVALID_ID; cch_arc_count];
401 for (cch_arc, slot) in full.iter_mut().enumerate() {
402 if presence.is_set(cch_arc as u64) {
403 let li = mapper.to_local(cch_arc as u64) as usize;
407 *slot = local[li];
408 }
409 }
410 full
411}
412
413#[allow(clippy::cast_possible_truncation)] fn expand_extra_csr(
421 presence: &BitVector,
422 mapper: &LocalIDMapper,
423 first_local: &[u32],
424 cch_arc_count: usize,
425) -> io::Result<Vec<u32>> {
426 let mut full = vec![0u32; cch_arc_count + 1];
427 for cch_arc in 0..cch_arc_count {
428 let count = if presence.is_set(cch_arc as u64) {
429 let le = mapper.to_local(cch_arc as u64) as usize;
433 let (lo, hi) = (first_local[le], first_local[le + 1]);
434 hi.checked_sub(lo).ok_or_else(|| {
437 io::Error::new(
438 io::ErrorKind::InvalidData,
439 "extra CSR offsets are not monotonic",
440 )
441 })?
442 } else {
443 0
444 };
445 full[cch_arc + 1] = full[cch_arc] + count;
450 }
451 Ok(full)
452}
453
454impl Cch {
455 pub fn save_struct(&self, path: &std::path::Path) -> io::Result<()> {
462 let file = std::fs::File::create(path)?;
463 let mut out = std::io::BufWriter::new(file);
464 self.write_struct(&mut out)?;
465 out.flush()
466 }
467
468 fn write_struct<W: Write>(&self, out: &mut W) -> io::Result<()> {
471 let node_count = self.node_count() as u64;
472 let cch_arc_count = self.cch_arc_count() as u64;
473 let input_arc_count = self.input_arc_to_cch_arc.len() as u64;
474
475 write_u64(out, STRUCT_MAGIC)?;
476 write_u32(out, FORMAT_VERSION)?;
477 write_u32(out, 0)?; write_u64(out, node_count)?;
479 write_u64(out, cch_arc_count)?;
480 write_u64(out, input_arc_count)?;
481
482 write_sized_vector(out, &self.order)?;
484 write_sized_vector(out, &self.rank)?;
485 write_sized_vector(out, &self.elimination_tree_parent)?;
486 write_sized_vector(out, &self.up_first_out)?;
487 write_sized_vector(out, &self.up_head)?;
488 write_sized_vector(out, &self.up_tail)?;
489 write_sized_vector(out, &self.down_first_out)?;
490 write_sized_vector(out, &self.down_head)?;
491 write_sized_vector(out, &self.down_to_up)?;
492 write_sized_vector(out, &self.input_arc_to_cch_arc)?;
493
494 let m = reconstruct_on_disk_mapping(self);
496
497 write_sized_bit_vector(out, &m.is_input_arc_upward)?;
498 write_sized_bit_vector(out, &m.does_cch_arc_have_input_arc)?;
499 write_sized_bit_vector(out, &m.does_cch_arc_have_extra_input_arc)?;
500
501 write_sized_vector(out, &m.forward_input_arc_of_cch)?;
502 write_sized_vector(out, &m.backward_input_arc_of_cch)?;
503 write_sized_vector(out, &m.first_extra_forward_input_arc_of_cch)?;
504 write_sized_vector(out, &m.first_extra_backward_input_arc_of_cch)?;
505 write_sized_vector(out, &m.extra_forward_input_arc_of_cch)?;
506 write_sized_vector(out, &m.extra_backward_input_arc_of_cch)?;
507
508 Ok(())
509 }
510
511 pub fn load_struct(path: &std::path::Path) -> io::Result<Cch> {
529 let bytes = std::fs::read(path)?;
530 Self::read_struct(&bytes)
531 }
532
533 #[allow(clippy::cast_possible_truncation)] #[allow(clippy::too_many_lines)] fn read_struct(bytes: &[u8]) -> io::Result<Cch> {
539 let mut r = StructReader::new(bytes);
540
541 let magic = r.read_u64()?;
542 if magic != STRUCT_MAGIC {
543 return Err(io::Error::new(
544 io::ErrorKind::InvalidData,
545 format!("bad magic in .cch-struct: {magic:#x}, expected {STRUCT_MAGIC:#x}"),
546 ));
547 }
548 let version = r.read_u32()?;
549 if version != FORMAT_VERSION {
550 return Err(io::Error::new(
551 io::ErrorKind::InvalidData,
552 format!("unsupported .cch-struct version {version}"),
553 ));
554 }
555 let _reserved = r.read_u32()?;
556 let node_count = r.read_u64_usize()?;
557 let cch_arc_count = r.read_u64_usize()?;
558 let input_arc_count = r.read_u64_usize()?;
559
560 let node_count_plus_1 = node_count.saturating_add(1);
563
564 let order = r.read_sized_vector("order", node_count)?;
566 let rank = r.read_sized_vector("rank", node_count)?;
567 let elimination_tree_parent = r.read_sized_vector("elimination_tree_parent", node_count)?;
568 let up_first_out = r.read_sized_vector("up_first_out", node_count_plus_1)?;
569 let up_head = r.read_sized_vector("up_head", cch_arc_count)?;
570 let up_tail = r.read_sized_vector("up_tail", cch_arc_count)?;
571 let down_first_out = r.read_sized_vector("down_first_out", node_count_plus_1)?;
572 let down_head = r.read_sized_vector("down_head", cch_arc_count)?;
573 let down_to_up = r.read_sized_vector("down_to_up", cch_arc_count)?;
574 let input_arc_to_cch_arc = r.read_sized_vector("input_arc_to_cch_arc", input_arc_count)?;
575
576 let _is_input_arc_upward =
578 r.read_sized_bit_vector("is_input_arc_upward", input_arc_count as u64)?;
579 let does_cch_arc_have_input_arc =
580 r.read_sized_bit_vector("does_cch_arc_have_input_arc", cch_arc_count as u64)?;
581 let does_cch_arc_have_extra_input_arc =
582 r.read_sized_bit_vector("does_cch_arc_have_extra_input_arc", cch_arc_count as u64)?;
583
584 let in_mapper = LocalIDMapper::new(
585 does_cch_arc_have_input_arc.words(),
586 does_cch_arc_have_input_arc.len(),
587 );
588 let extra_mapper = LocalIDMapper::new(
589 does_cch_arc_have_extra_input_arc.words(),
590 does_cch_arc_have_extra_input_arc.len(),
591 );
592
593 let local_in = in_mapper.local_id_count() as usize;
597 let local_extra = extra_mapper.local_id_count() as usize;
598 let local_extra_plus_1 = local_extra.saturating_add(1);
599
600 let forward_local = r.read_sized_vector("forward_input_arc_of_cch", local_in)?;
601 let backward_local = r.read_sized_vector("backward_input_arc_of_cch", local_in)?;
602 let first_extra_forward_local =
603 r.read_sized_vector("first_extra_forward_input_arc_of_cch", local_extra_plus_1)?;
604 let first_extra_backward_local =
605 r.read_sized_vector("first_extra_backward_input_arc_of_cch", local_extra_plus_1)?;
606 let extra_forward_input_arc_of_cch =
609 r.read_sized_vector_any("extra_forward_input_arc_of_cch")?;
610 let extra_backward_input_arc_of_cch =
611 r.read_sized_vector_any("extra_backward_input_arc_of_cch")?;
612
613 let forward_input_arc_of_cch = expand_local_to_full(
615 &does_cch_arc_have_input_arc,
616 &in_mapper,
617 &forward_local,
618 cch_arc_count,
619 );
620 let backward_input_arc_of_cch = expand_local_to_full(
621 &does_cch_arc_have_input_arc,
622 &in_mapper,
623 &backward_local,
624 cch_arc_count,
625 );
626 let first_extra_forward_input_arc_of_cch = expand_extra_csr(
627 &does_cch_arc_have_extra_input_arc,
628 &extra_mapper,
629 &first_extra_forward_local,
630 cch_arc_count,
631 )?;
632 let first_extra_backward_input_arc_of_cch = expand_extra_csr(
633 &does_cch_arc_have_extra_input_arc,
634 &extra_mapper,
635 &first_extra_backward_local,
636 cch_arc_count,
637 )?;
638
639 if first_extra_forward_input_arc_of_cch[cch_arc_count] as usize
643 != extra_forward_input_arc_of_cch.len()
644 {
645 return Err(io::Error::new(
646 io::ErrorKind::InvalidData,
647 "forward extra CSR total disagrees with extra list length",
648 ));
649 }
650 if first_extra_backward_input_arc_of_cch[cch_arc_count] as usize
651 != extra_backward_input_arc_of_cch.len()
652 {
653 return Err(io::Error::new(
654 io::ErrorKind::InvalidData,
655 "backward extra CSR total disagrees with extra list length",
656 ));
657 }
658
659 Ok(Cch {
660 rank,
661 order,
662 elimination_tree_parent,
663 up_first_out,
664 up_head,
665 up_tail,
666 down_first_out,
667 down_head,
668 down_to_up,
669 input_arc_to_cch_arc,
670 forward_input_arc_of_cch,
671 backward_input_arc_of_cch,
672 first_extra_forward_input_arc_of_cch,
673 extra_forward_input_arc_of_cch,
674 first_extra_backward_input_arc_of_cch,
675 extra_backward_input_arc_of_cch,
676 })
677 }
678}
679
680impl Metric {
681 pub fn save(&self, path: &std::path::Path) -> io::Result<()> {
687 let file = std::fs::File::create(path)?;
688 let mut out = std::io::BufWriter::new(file);
689 self.write_metric(&mut out)?;
690 out.flush()
691 }
692
693 fn write_metric<W: Write>(&self, out: &mut W) -> io::Result<()> {
695 let cch_arc_count = self.forward.len() as u64;
696 write_u64(out, METRIC_MAGIC)?;
697 write_u32(out, FORMAT_VERSION)?;
698 write_u32(out, 0)?; write_u64(out, cch_arc_count)?;
700 write_sized_vector(out, &self.forward)?;
701 write_sized_vector(out, &self.backward)?;
702 Ok(())
703 }
704}
705
706#[cfg(test)]
707mod tests {
708 use super::*;
709 use crate::INF_WEIGHT;
710 use crate::graph::Graph;
711
712 fn csr(node_count: u32, tail: &[u32], head: &[u32]) -> Graph {
714 let n = node_count as usize;
715 let mut degree = vec![0u32; n];
716 for &t in tail {
717 degree[t as usize] += 1;
718 }
719 let mut first_out = vec![0u32; n + 1];
720 for v in 0..n {
721 first_out[v + 1] = first_out[v] + degree[v];
722 }
723 let mut next: Vec<u32> = first_out[..n].to_vec();
724 let mut g_head = vec![0u32; head.len()];
725 for (&t, &h) in tail.iter().zip(head.iter()) {
726 let slot = next[t as usize] as usize;
727 g_head[slot] = h;
728 next[t as usize] += 1;
729 }
730 Graph {
731 first_out,
732 head: g_head,
733 weight: vec![1u32; head.len()],
734 }
735 }
736
737 fn to_bytes(c: &Cch) -> Vec<u8> {
742 let dir = tempfile::tempdir().expect("tempdir");
743 let path = dir.path().join("tmp.cch-struct");
744 c.save_struct(&path).expect("save_struct");
745 std::fs::read(&path).expect("read back struct bytes")
746 }
747
748 fn assert_round_trip(name: &str, c: &Cch, weight_sets: &[Vec<u32>]) {
751 let bytes = to_bytes(c);
752 let loaded = Cch::read_struct(&bytes).expect("read_struct");
753
754 assert_eq!(loaded.rank, c.rank, "[{name}] rank");
756 assert_eq!(loaded.order, c.order, "[{name}] order");
757 assert_eq!(
758 loaded.elimination_tree_parent, c.elimination_tree_parent,
759 "[{name}] elim"
760 );
761 assert_eq!(loaded.up_first_out, c.up_first_out, "[{name}] up_first_out");
762 assert_eq!(loaded.up_head, c.up_head, "[{name}] up_head");
763 assert_eq!(loaded.up_tail, c.up_tail, "[{name}] up_tail");
764 assert_eq!(
765 loaded.down_first_out, c.down_first_out,
766 "[{name}] down_first_out"
767 );
768 assert_eq!(loaded.down_head, c.down_head, "[{name}] down_head");
769 assert_eq!(loaded.down_to_up, c.down_to_up, "[{name}] down_to_up");
770 assert_eq!(
771 loaded.input_arc_to_cch_arc, c.input_arc_to_cch_arc,
772 "[{name}] input_arc_to_cch_arc"
773 );
774
775 for (i, w) in weight_sets.iter().enumerate() {
777 let want = c.customize(w);
778 let got = loaded.customize(w);
779 assert_eq!(want.forward, got.forward, "[{name}] forward weights #{i}");
780 assert_eq!(
781 want.backward, got.backward,
782 "[{name}] backward weights #{i}"
783 );
784 }
785 }
786
787 #[test]
788 fn round_trip_path_identity() {
789 let n = 5u32;
790 let tail = vec![0u32, 1, 1, 2, 2, 3, 3, 4];
791 let head = vec![1u32, 0, 2, 1, 3, 2, 4, 3];
792 let c = Cch::build(&csr(n, &tail, &head), &(0..n).collect::<Vec<_>>());
793 let ws = vec![
794 vec![10u32, 11, 20, 21, 30, 31, 40, 41],
795 vec![1u32, 1, 1, 1, 1, 1, 1, 1],
796 vec![INF_WEIGHT, 5, INF_WEIGHT, 7, 9, INF_WEIGHT, 2, 3],
797 ];
798 assert_round_trip("path_identity", &c, &ws);
799 }
800
801 #[test]
802 fn round_trip_fillin_nonidentity_order() {
803 let n = 4u32;
804 let tail = vec![0u32, 0, 0, 1, 2, 3];
805 let head = vec![1u32, 2, 3, 0, 0, 0];
806 let order = vec![0u32, 1, 2, 3];
807 let c = Cch::build(&csr(n, &tail, &head), &order);
808 let ws = vec![vec![5u32, 7, 9, 6, 8, 10], vec![1u32, 2, 3, 4, 5, 6]];
809 assert_round_trip("fillin", &c, &ws);
810 }
811
812 #[test]
813 fn round_trip_parallel_arcs() {
814 let n = 4u32;
816 let tail = vec![0u32, 0, 1, 1, 1, 2, 2, 3];
817 let head = vec![1u32, 1, 0, 0, 2, 1, 3, 2];
818 let order = vec![2u32, 0, 3, 1];
819 let c = Cch::build(&csr(n, &tail, &head), &order);
820 let extra_total =
822 c.extra_forward_input_arc_of_cch.len() + c.extra_backward_input_arc_of_cch.len();
823 assert!(
824 extra_total > 0,
825 "fixture must have parallel arcs in the extra lists"
826 );
827 let ws = vec![
828 vec![50u32, 9, 40, 8, 17, 18, 19, 20],
829 vec![9u32, 50, 8, 40, 1, 2, 3, 4],
830 ];
831 assert_round_trip("parallel_arcs", &c, &ws);
832 }
833
834 #[test]
835 fn round_trip_grid_block_boundary() {
836 let cols = 24u32;
839 let rows = 24u32;
840 let n = cols * rows;
841 let mut tail = Vec::new();
842 let mut head = Vec::new();
843 for r in 0..rows {
844 for c in 0..cols {
845 let v = r * cols + c;
846 if c + 1 < cols {
847 tail.push(v);
848 head.push(v + 1);
849 }
850 if c > 0 {
851 tail.push(v);
852 head.push(v - 1);
853 }
854 if r + 1 < rows {
855 tail.push(v);
856 head.push(v + cols);
857 }
858 if r > 0 {
859 tail.push(v);
860 head.push(v - cols);
861 }
862 }
863 }
864 let graph = csr(n, &tail, &head);
865 let order = crate::degree_order(&graph);
866 let c = Cch::build(&graph, &order);
867 assert!(c.cch_arc_count() > 512, "grid must exceed 512 CCH arcs");
868 #[allow(clippy::cast_possible_truncation)]
869 let w: Vec<u32> = (0..tail.len() as u32)
870 .map(|i| (i * 7 + 1) % 9973 + 1)
871 .collect();
872 assert_round_trip("grid_24x24", &c, &[w]);
873 }
874
875 #[test]
876 fn round_trip_empty_and_single_node() {
877 let n = 4u32;
879 let c = Cch::build(&csr(n, &[], &[]), &(0..n).collect::<Vec<_>>());
880 assert_round_trip("empty_arcs", &c, &[vec![]]);
881
882 let c = Cch::build(&csr(1, &[], &[]), &[0]);
884 assert_round_trip("single_node", &c, &[vec![]]);
885 }
886
887 fn valid_struct_bytes() -> Vec<u8> {
893 let n = 4u32;
894 let tail = vec![0u32, 0, 1, 1, 1, 2, 2, 3];
895 let head = vec![1u32, 1, 0, 0, 2, 1, 3, 2];
896 let c = Cch::build(&csr(n, &tail, &head), &[0, 1, 2, 3]);
897 to_bytes(&c)
898 }
899
900 fn assert_invalid(bytes: &[u8], must_contain: &str) {
901 let err = Cch::read_struct(bytes).map(|_| ()).unwrap_err();
902 assert_eq!(
903 err.kind(),
904 io::ErrorKind::InvalidData,
905 "expected InvalidData, got: {err}"
906 );
907 assert!(
908 err.to_string().contains(must_contain),
909 "error '{err}' must contain '{must_contain}'"
910 );
911 }
912
913 #[test]
914 fn corrupt_baseline_is_valid() {
915 Cch::read_struct(&valid_struct_bytes()).expect("baseline must be valid");
917 }
918
919 #[test]
920 fn corrupt_empty_buffer_truncated() {
921 assert_invalid(&[], "truncated");
922 }
923
924 #[test]
925 fn corrupt_bad_magic() {
926 let mut b = valid_struct_bytes();
927 b[0..8].copy_from_slice(&0xDEAD_BEEF_DEAD_BEEFu64.to_le_bytes());
928 assert_invalid(&b, "bad magic");
929 }
930
931 #[test]
932 fn corrupt_bad_version() {
933 let mut b = valid_struct_bytes();
934 b[8..12].copy_from_slice(&2u32.to_le_bytes());
935 assert_invalid(&b, "unsupported .cch-struct version");
936 }
937
938 #[test]
939 fn corrupt_truncated_after_header() {
940 let b = valid_struct_bytes();
943 assert_invalid(&b[..40], "truncated");
944 }
945
946 #[test]
947 fn corrupt_truncated_mid_section() {
948 let b = valid_struct_bytes();
950 assert_invalid(&b[..50], "order");
952 }
953
954 #[test]
955 fn corrupt_inflated_node_count() {
956 let mut b = valid_struct_bytes();
958 let nc = {
959 let mut buf = [0u8; 8];
960 buf.copy_from_slice(&b[16..24]);
961 u64::from_le_bytes(buf)
962 };
963 b[16..24].copy_from_slice(&(nc + 1_000_000).to_le_bytes());
964 assert_invalid(&b, "does not match expected");
965 }
966
967 #[test]
968 fn corrupt_node_count_absurd() {
969 let mut b = valid_struct_bytes();
973 b[16..24].copy_from_slice(&u64::MAX.to_le_bytes());
974 assert_invalid(&b, "does not match expected");
975 }
976
977 #[test]
978 fn corrupt_order_byte_length_not_multiple_of_4() {
979 let mut b = valid_struct_bytes();
982 b[16..24].copy_from_slice(&0u64.to_le_bytes()); b[24..32].copy_from_slice(&0u64.to_le_bytes()); b[32..40].copy_from_slice(&0u64.to_le_bytes()); let mut crafted = b[..40].to_vec();
991 crafted.extend_from_slice(&3u64.to_le_bytes());
992 crafted.extend_from_slice(&[0u8, 0, 0]);
993 assert_invalid(&crafted, "not a multiple of 4");
994 }
995
996 #[test]
997 fn corrupt_bitvector_byte_length_mismatch() {
998 let mut b = valid_struct_bytes();
1003 let off = first_bitvector_byte_length_offset(&b);
1004 b[off + 8..off + 16].copy_from_slice(&999u64.to_le_bytes());
1007 assert_invalid(&b, "bitvector byte_length");
1008 }
1009
1010 fn read_u64_usize(b: &[u8], o: usize) -> usize {
1012 let mut buf = [0u8; 8];
1013 buf.copy_from_slice(&b[o..o + 8]);
1014 usize::try_from(u64::from_le_bytes(buf)).expect("offset fits usize in test")
1015 }
1016
1017 fn first_bitvector_byte_length_offset(b: &[u8]) -> usize {
1020 let mut pos = 40usize;
1021 for _ in 0..10 {
1022 pos += 8 + read_u64_usize(b, pos);
1023 }
1024 pos
1025 }
1026
1027 #[test]
1028 fn corrupt_extra_csr_total_mismatch() {
1029 let n = 4u32;
1037 let tail = vec![0u32, 0, 1, 1, 1, 2, 2, 3];
1038 let head = vec![1u32, 1, 0, 0, 2, 1, 3, 2];
1039 let c = Cch::build(&csr(n, &tail, &head), &[0, 1, 2, 3]);
1040 assert!(
1041 !c.extra_forward_input_arc_of_cch.is_empty(),
1042 "need a non-empty forward extra list"
1043 );
1044 let mut b = to_bytes(&c);
1045 let off = extra_forward_byte_length_offset(&b);
1048 let cur = read_u64_usize(&b, off);
1051 assert!(cur >= 4);
1052 let shrunk = u64::try_from(cur - 4).expect("fits");
1053 b[off..off + 8].copy_from_slice(&shrunk.to_le_bytes());
1054 b.drain(off + 8 + (cur - 4)..off + 8 + cur);
1057 assert_invalid(&b, "extra CSR total disagrees");
1058 }
1059
1060 fn extra_forward_byte_length_offset(b: &[u8]) -> usize {
1062 let mut pos = 40usize;
1063 for _ in 0..10 {
1065 pos += 8 + read_u64_usize(b, pos);
1066 }
1067 for _ in 0..3 {
1069 pos += 16 + read_u64_usize(b, pos + 8);
1070 }
1071 for _ in 0..4 {
1073 pos += 8 + read_u64_usize(b, pos);
1074 }
1075 pos
1077 }
1078
1079 fn section_start_offsets(b: &[u8]) -> Vec<usize> {
1082 let mut starts = Vec::with_capacity(19);
1083 let mut pos = 40usize;
1084 for _ in 0..10 {
1086 starts.push(pos);
1087 pos += 8 + read_u64_usize(b, pos);
1088 }
1089 for _ in 0..3 {
1091 starts.push(pos);
1092 pos += 16 + read_u64_usize(b, pos + 8);
1093 }
1094 for _ in 0..6 {
1096 starts.push(pos);
1097 pos += 8 + read_u64_usize(b, pos);
1098 }
1099 starts
1100 }
1101
1102 #[test]
1103 fn corrupt_truncate_at_each_section_start() {
1104 let b = valid_struct_bytes();
1108 for (i, &start) in section_start_offsets(&b).iter().enumerate() {
1109 let truncated = &b[..start];
1110 let err = Cch::read_struct(truncated).map(|_| ()).unwrap_err();
1111 assert_eq!(
1112 err.kind(),
1113 io::ErrorKind::InvalidData,
1114 "section #{i} truncation must be InvalidData (got {err})"
1115 );
1116 }
1117 }
1118
1119 #[test]
1120 fn corrupt_truncated_u32_field() {
1121 let b = valid_struct_bytes();
1124 assert_invalid(&b[..9], "truncated");
1125 }
1126
1127 #[test]
1128 fn corrupt_bitvector_bit_count_mismatch() {
1129 let mut b = valid_struct_bytes();
1133 let starts = section_start_offsets(&b);
1134 let off = starts[11];
1136 let real_bits = read_u64_usize(&b, off) as u64;
1137 assert!(real_bits > 0, "fixture must have CCH arcs");
1138 b[off..off + 8].copy_from_slice(&(real_bits + 1).to_le_bytes());
1142 assert_invalid(&b, "does not match expected");
1143 }
1144
1145 #[test]
1146 fn corrupt_bitvector_data_truncated() {
1147 let b = valid_struct_bytes();
1152 let starts = section_start_offsets(&b);
1153 let off = starts[10]; let byte_length = read_u64_usize(&b, off + 8);
1155 assert!(byte_length > 0, "bitvector must have data to truncate");
1156 let cut = off + 16 + 8;
1158 assert!(cut < off + 16 + byte_length, "must truncate mid-data");
1159 assert_invalid(&b[..cut], "is_input_arc_upward");
1160 }
1161
1162 #[test]
1163 fn corrupt_extra_csr_non_monotonic() {
1164 let n = 4u32;
1167 let tail = vec![0u32, 0, 1, 1, 1, 2, 2, 3];
1168 let head = vec![1u32, 1, 0, 0, 2, 1, 3, 2];
1169 let c = Cch::build(&csr(n, &tail, &head), &[0, 1, 2, 3]);
1170 assert!(
1171 !c.extra_forward_input_arc_of_cch.is_empty(),
1172 "need a non-empty forward extra list"
1173 );
1174 let mut b = to_bytes(&c);
1175 let starts = section_start_offsets(&b);
1176 let data = starts[15] + 8;
1180 b[data..data + 4].copy_from_slice(&0xFFFF_FFFFu32.to_le_bytes());
1181 assert_invalid(&b, "not monotonic");
1182 }
1183
1184 #[test]
1185 fn corrupt_backward_extra_csr_non_monotonic() {
1186 let n = 4u32;
1189 let tail = vec![0u32, 0, 1, 1, 1, 2, 2, 3];
1190 let head = vec![1u32, 1, 0, 0, 2, 1, 3, 2];
1191 let c = Cch::build(&csr(n, &tail, &head), &[0, 1, 2, 3]);
1192 assert!(
1193 !c.extra_backward_input_arc_of_cch.is_empty(),
1194 "need a non-empty backward extra list"
1195 );
1196 let mut b = to_bytes(&c);
1197 let starts = section_start_offsets(&b);
1198 let data = starts[16] + 8;
1200 b[data..data + 4].copy_from_slice(&0xFFFF_FFFFu32.to_le_bytes());
1201 assert_invalid(&b, "not monotonic");
1202 }
1203
1204 #[test]
1205 fn corrupt_backward_extra_csr_total_mismatch() {
1206 let n = 4u32;
1209 let tail = vec![0u32, 0, 1, 1, 1, 2, 2, 3];
1210 let head = vec![1u32, 1, 0, 0, 2, 1, 3, 2];
1211 let c = Cch::build(&csr(n, &tail, &head), &[0, 1, 2, 3]);
1212 assert!(
1213 !c.extra_backward_input_arc_of_cch.is_empty(),
1214 "need a non-empty backward extra list"
1215 );
1216 let mut b = to_bytes(&c);
1217 let starts = section_start_offsets(&b);
1218 let off = starts[18];
1220 let cur = read_u64_usize(&b, off);
1221 assert!(cur >= 4);
1222 let shrunk = u64::try_from(cur - 4).expect("fits");
1223 b[off..off + 8].copy_from_slice(&shrunk.to_le_bytes());
1224 b.drain(off + 8 + (cur - 4)..off + 8 + cur);
1225 assert_invalid(&b, "backward extra CSR total disagrees");
1226 }
1227}