use std::io::{self, Write};
use crate::bundle::INVALID_ID;
use crate::customize::Metric;
use crate::internal::bitvec::BitVector;
use crate::internal::id_map::LocalIDMapper;
use crate::structure::Cch;
const STRUCT_MAGIC: u64 = 0x4343_485F_5354_5243; const METRIC_MAGIC: u64 = 0x4343_485F_4D45_5452; const FORMAT_VERSION: u32 = 1;
fn write_u32<W: Write>(out: &mut W, value: u32) -> io::Result<()> {
out.write_all(&value.to_le_bytes())
}
fn write_u64<W: Write>(out: &mut W, value: u64) -> io::Result<()> {
out.write_all(&value.to_le_bytes())
}
fn write_sized_vector<W: Write>(out: &mut W, v: &[u32]) -> io::Result<()> {
let byte_length = (v.len() as u64) * 4;
write_u64(out, byte_length)?;
let mut bytes = Vec::with_capacity(v.len() * 4);
for &x in v {
bytes.extend_from_slice(&x.to_le_bytes());
}
out.write_all(&bytes)
}
fn write_sized_bit_vector<W: Write>(out: &mut W, bv: &BitVector) -> io::Result<()> {
let bit_count = bv.len();
let byte_length = bit_count.div_ceil(512) * 64;
write_u64(out, bit_count)?;
write_u64(out, byte_length)?;
let word_count = (byte_length / 8) as usize;
let mut bytes = Vec::with_capacity(word_count * 8);
let words = bv.words();
for &w in words {
bytes.extend_from_slice(&w.to_le_bytes());
}
for _ in words.len()..word_count {
bytes.extend_from_slice(&0u64.to_le_bytes());
}
out.write_all(&bytes)
}
struct OnDiskMapping {
is_input_arc_upward: BitVector,
does_cch_arc_have_input_arc: BitVector,
does_cch_arc_have_extra_input_arc: BitVector,
forward_input_arc_of_cch: Vec<u32>,
backward_input_arc_of_cch: Vec<u32>,
first_extra_forward_input_arc_of_cch: Vec<u32>,
first_extra_backward_input_arc_of_cch: Vec<u32>,
extra_forward_input_arc_of_cch: Vec<u32>,
extra_backward_input_arc_of_cch: Vec<u32>,
}
#[allow(clippy::cast_possible_truncation)] fn reconstruct_on_disk_mapping(cch: &Cch) -> OnDiskMapping {
let input_arc_count = cch.input_arc_to_cch_arc.len();
let cch_arc_count = cch.cch_arc_count();
let mut is_input_arc_upward = BitVector::new(input_arc_count as u64);
for &ia in &cch.forward_input_arc_of_cch {
if ia != INVALID_ID {
is_input_arc_upward.set(u64::from(ia));
}
}
for &ia in &cch.extra_forward_input_arc_of_cch {
is_input_arc_upward.set(u64::from(ia));
}
let mut does_cch_arc_have_input_arc = BitVector::new(cch_arc_count as u64);
for cch_arc in 0..cch_arc_count {
if cch.forward_input_arc_of_cch[cch_arc] != INVALID_ID
|| cch.backward_input_arc_of_cch[cch_arc] != INVALID_ID
{
does_cch_arc_have_input_arc.set(cch_arc as u64);
}
}
let mut does_cch_arc_have_extra_input_arc = BitVector::new(cch_arc_count as u64);
let ff = &cch.first_extra_forward_input_arc_of_cch;
let fb = &cch.first_extra_backward_input_arc_of_cch;
for cch_arc in 0..cch_arc_count {
if ff[cch_arc + 1] > ff[cch_arc] || fb[cch_arc + 1] > fb[cch_arc] {
does_cch_arc_have_extra_input_arc.set(cch_arc as u64);
}
}
let in_mapper = LocalIDMapper::new(
does_cch_arc_have_input_arc.words(),
does_cch_arc_have_input_arc.len(),
);
let local_in = in_mapper.local_id_count() as usize;
let mut forward_local = vec![INVALID_ID; local_in];
let mut backward_local = vec![INVALID_ID; local_in];
for cch_arc in 0..cch_arc_count {
if does_cch_arc_have_input_arc.is_set(cch_arc as u64) {
let li = in_mapper.to_local(cch_arc as u64) as usize;
forward_local[li] = cch.forward_input_arc_of_cch[cch_arc];
backward_local[li] = cch.backward_input_arc_of_cch[cch_arc];
}
}
let extra_mapper = LocalIDMapper::new(
does_cch_arc_have_extra_input_arc.words(),
does_cch_arc_have_extra_input_arc.len(),
);
let local_extra = extra_mapper.local_id_count() as usize;
let mut first_extra_forward = vec![0u32; local_extra + 1];
let mut first_extra_backward = vec![0u32; local_extra + 1];
for cch_arc in 0..cch_arc_count {
if does_cch_arc_have_extra_input_arc.is_set(cch_arc as u64) {
let li = extra_mapper.to_local(cch_arc as u64) as usize;
first_extra_forward[li + 1] = ff[cch_arc + 1] - ff[cch_arc];
first_extra_backward[li + 1] = fb[cch_arc + 1] - fb[cch_arc];
}
}
for i in 0..local_extra {
first_extra_forward[i + 1] += first_extra_forward[i];
first_extra_backward[i + 1] += first_extra_backward[i];
}
OnDiskMapping {
is_input_arc_upward,
does_cch_arc_have_input_arc,
does_cch_arc_have_extra_input_arc,
forward_input_arc_of_cch: forward_local,
backward_input_arc_of_cch: backward_local,
first_extra_forward_input_arc_of_cch: first_extra_forward,
first_extra_backward_input_arc_of_cch: first_extra_backward,
extra_forward_input_arc_of_cch: cch.extra_forward_input_arc_of_cch.clone(),
extra_backward_input_arc_of_cch: cch.extra_backward_input_arc_of_cch.clone(),
}
}
struct StructReader<'a> {
bytes: &'a [u8],
pos: usize,
}
impl<'a> StructReader<'a> {
fn new(bytes: &'a [u8]) -> Self {
Self { bytes, pos: 0 }
}
fn read_u64(&mut self) -> io::Result<u64> {
let end = self.pos.saturating_add(8);
if end > self.bytes.len() {
return Err(truncated_err("u64 field"));
}
let mut buf = [0u8; 8];
buf.copy_from_slice(&self.bytes[self.pos..end]);
self.pos = end;
Ok(u64::from_le_bytes(buf))
}
#[allow(clippy::cast_possible_truncation)] fn read_u64_usize(&mut self) -> io::Result<usize> {
Ok(self.read_u64()? as usize)
}
fn read_u32(&mut self) -> io::Result<u32> {
let end = self.pos.saturating_add(4);
if end > self.bytes.len() {
return Err(truncated_err("u32 field"));
}
let mut buf = [0u8; 4];
buf.copy_from_slice(&self.bytes[self.pos..end]);
self.pos = end;
Ok(u32::from_le_bytes(buf))
}
fn read_sized_vector(&mut self, label: &str, expected_count: usize) -> io::Result<Vec<u32>> {
self.read_sized_vector_inner(label, Some(expected_count))
}
fn read_sized_vector_any(&mut self, label: &str) -> io::Result<Vec<u32>> {
self.read_sized_vector_inner(label, None)
}
fn read_sized_vector_inner(
&mut self,
label: &str,
expected_count: Option<usize>,
) -> io::Result<Vec<u32>> {
let byte_length = self.read_u64_usize()?;
if byte_length % 4 != 0 {
return Err(io::Error::new(
io::ErrorKind::InvalidData,
format!("section '{label}' byte_length {byte_length} is not a multiple of 4"),
));
}
let count = byte_length / 4;
if let Some(expected) = expected_count {
if expected.checked_mul(4) != Some(byte_length) {
return Err(io::Error::new(
io::ErrorKind::InvalidData,
format!(
"section '{label}' byte_length {byte_length} does not match expected \
4 * {expected}; header count and section length disagree"
),
));
}
}
let end = self.pos.saturating_add(byte_length);
if end > self.bytes.len() {
return Err(truncated_err(label));
}
let mut v = Vec::with_capacity(count);
let mut off = self.pos;
for _ in 0..count {
let mut buf = [0u8; 4];
buf.copy_from_slice(&self.bytes[off..off + 4]);
v.push(u32::from_le_bytes(buf));
off += 4;
}
self.pos = end;
Ok(v)
}
#[allow(clippy::cast_possible_truncation)] fn read_sized_bit_vector(&mut self, label: &str, expected_bits: u64) -> io::Result<BitVector> {
let bit_count = self.read_u64()?;
if bit_count != expected_bits {
return Err(io::Error::new(
io::ErrorKind::InvalidData,
format!(
"section '{label}' bit_count {bit_count} does not match expected \
{expected_bits}"
),
));
}
let byte_length = self.read_u64_usize()?;
let expected_byte_length = (byte_length_for_bits(bit_count)).unwrap_or(usize::MAX);
if byte_length != expected_byte_length {
return Err(io::Error::new(
io::ErrorKind::InvalidData,
format!(
"section '{label}' bitvector byte_length {byte_length} does not match \
expected {expected_byte_length} for bit_count {bit_count}"
),
));
}
let end = self.pos.saturating_add(byte_length);
if end > self.bytes.len() {
return Err(truncated_err(label));
}
let word_count = (bit_count.div_ceil(64)) as usize;
let mut bv = BitVector::new(bit_count);
let mut off = self.pos;
for word_idx in 0..word_count {
let mut buf = [0u8; 8];
buf.copy_from_slice(&self.bytes[off..off + 8]);
let word = u64::from_le_bytes(buf);
let base = (word_idx as u64) * 64;
let mut w = word;
while w != 0 {
let b = u64::from(w.trailing_zeros());
let global = base + b;
if global < bit_count {
bv.set(global);
}
w &= w - 1;
}
off += 8;
}
self.pos = end;
Ok(bv)
}
}
fn truncated_err(label: &str) -> io::Error {
io::Error::new(
io::ErrorKind::InvalidData,
format!("truncated .cch-struct at section '{label}'"),
)
}
fn byte_length_for_bits(bit_count: u64) -> Option<usize> {
usize::try_from(bit_count.div_ceil(512))
.ok()
.and_then(|blocks| blocks.checked_mul(64))
}
#[allow(clippy::cast_possible_truncation)] fn expand_local_to_full(
presence: &BitVector,
mapper: &LocalIDMapper,
local: &[u32],
cch_arc_count: usize,
) -> Vec<u32> {
let mut full = vec![INVALID_ID; cch_arc_count];
for (cch_arc, slot) in full.iter_mut().enumerate() {
if presence.is_set(cch_arc as u64) {
let li = mapper.to_local(cch_arc as u64) as usize;
*slot = local[li];
}
}
full
}
#[allow(clippy::cast_possible_truncation)] fn expand_extra_csr(
presence: &BitVector,
mapper: &LocalIDMapper,
first_local: &[u32],
cch_arc_count: usize,
) -> io::Result<Vec<u32>> {
let mut full = vec![0u32; cch_arc_count + 1];
for cch_arc in 0..cch_arc_count {
let count = if presence.is_set(cch_arc as u64) {
let le = mapper.to_local(cch_arc as u64) as usize;
let (lo, hi) = (first_local[le], first_local[le + 1]);
hi.checked_sub(lo).ok_or_else(|| {
io::Error::new(
io::ErrorKind::InvalidData,
"extra CSR offsets are not monotonic",
)
})?
} else {
0
};
full[cch_arc + 1] = full[cch_arc] + count;
}
Ok(full)
}
impl Cch {
pub fn save_struct(&self, path: &std::path::Path) -> io::Result<()> {
let file = std::fs::File::create(path)?;
let mut out = std::io::BufWriter::new(file);
self.write_struct(&mut out)?;
out.flush()
}
fn write_struct<W: Write>(&self, out: &mut W) -> io::Result<()> {
let node_count = self.node_count() as u64;
let cch_arc_count = self.cch_arc_count() as u64;
let input_arc_count = self.input_arc_to_cch_arc.len() as u64;
write_u64(out, STRUCT_MAGIC)?;
write_u32(out, FORMAT_VERSION)?;
write_u32(out, 0)?; write_u64(out, node_count)?;
write_u64(out, cch_arc_count)?;
write_u64(out, input_arc_count)?;
write_sized_vector(out, &self.order)?;
write_sized_vector(out, &self.rank)?;
write_sized_vector(out, &self.elimination_tree_parent)?;
write_sized_vector(out, &self.up_first_out)?;
write_sized_vector(out, &self.up_head)?;
write_sized_vector(out, &self.up_tail)?;
write_sized_vector(out, &self.down_first_out)?;
write_sized_vector(out, &self.down_head)?;
write_sized_vector(out, &self.down_to_up)?;
write_sized_vector(out, &self.input_arc_to_cch_arc)?;
let m = reconstruct_on_disk_mapping(self);
write_sized_bit_vector(out, &m.is_input_arc_upward)?;
write_sized_bit_vector(out, &m.does_cch_arc_have_input_arc)?;
write_sized_bit_vector(out, &m.does_cch_arc_have_extra_input_arc)?;
write_sized_vector(out, &m.forward_input_arc_of_cch)?;
write_sized_vector(out, &m.backward_input_arc_of_cch)?;
write_sized_vector(out, &m.first_extra_forward_input_arc_of_cch)?;
write_sized_vector(out, &m.first_extra_backward_input_arc_of_cch)?;
write_sized_vector(out, &m.extra_forward_input_arc_of_cch)?;
write_sized_vector(out, &m.extra_backward_input_arc_of_cch)?;
Ok(())
}
pub fn load_struct(path: &std::path::Path) -> io::Result<Cch> {
let bytes = std::fs::read(path)?;
Self::read_struct(&bytes)
}
#[allow(clippy::cast_possible_truncation)] #[allow(clippy::too_many_lines)] fn read_struct(bytes: &[u8]) -> io::Result<Cch> {
let mut r = StructReader::new(bytes);
let magic = r.read_u64()?;
if magic != STRUCT_MAGIC {
return Err(io::Error::new(
io::ErrorKind::InvalidData,
format!("bad magic in .cch-struct: {magic:#x}, expected {STRUCT_MAGIC:#x}"),
));
}
let version = r.read_u32()?;
if version != FORMAT_VERSION {
return Err(io::Error::new(
io::ErrorKind::InvalidData,
format!("unsupported .cch-struct version {version}"),
));
}
let _reserved = r.read_u32()?;
let node_count = r.read_u64_usize()?;
let cch_arc_count = r.read_u64_usize()?;
let input_arc_count = r.read_u64_usize()?;
let node_count_plus_1 = node_count.saturating_add(1);
let order = r.read_sized_vector("order", node_count)?;
let rank = r.read_sized_vector("rank", node_count)?;
let elimination_tree_parent = r.read_sized_vector("elimination_tree_parent", node_count)?;
let up_first_out = r.read_sized_vector("up_first_out", node_count_plus_1)?;
let up_head = r.read_sized_vector("up_head", cch_arc_count)?;
let up_tail = r.read_sized_vector("up_tail", cch_arc_count)?;
let down_first_out = r.read_sized_vector("down_first_out", node_count_plus_1)?;
let down_head = r.read_sized_vector("down_head", cch_arc_count)?;
let down_to_up = r.read_sized_vector("down_to_up", cch_arc_count)?;
let input_arc_to_cch_arc = r.read_sized_vector("input_arc_to_cch_arc", input_arc_count)?;
let _is_input_arc_upward =
r.read_sized_bit_vector("is_input_arc_upward", input_arc_count as u64)?;
let does_cch_arc_have_input_arc =
r.read_sized_bit_vector("does_cch_arc_have_input_arc", cch_arc_count as u64)?;
let does_cch_arc_have_extra_input_arc =
r.read_sized_bit_vector("does_cch_arc_have_extra_input_arc", cch_arc_count as u64)?;
let in_mapper = LocalIDMapper::new(
does_cch_arc_have_input_arc.words(),
does_cch_arc_have_input_arc.len(),
);
let extra_mapper = LocalIDMapper::new(
does_cch_arc_have_extra_input_arc.words(),
does_cch_arc_have_extra_input_arc.len(),
);
let local_in = in_mapper.local_id_count() as usize;
let local_extra = extra_mapper.local_id_count() as usize;
let local_extra_plus_1 = local_extra.saturating_add(1);
let forward_local = r.read_sized_vector("forward_input_arc_of_cch", local_in)?;
let backward_local = r.read_sized_vector("backward_input_arc_of_cch", local_in)?;
let first_extra_forward_local =
r.read_sized_vector("first_extra_forward_input_arc_of_cch", local_extra_plus_1)?;
let first_extra_backward_local =
r.read_sized_vector("first_extra_backward_input_arc_of_cch", local_extra_plus_1)?;
let extra_forward_input_arc_of_cch =
r.read_sized_vector_any("extra_forward_input_arc_of_cch")?;
let extra_backward_input_arc_of_cch =
r.read_sized_vector_any("extra_backward_input_arc_of_cch")?;
let forward_input_arc_of_cch = expand_local_to_full(
&does_cch_arc_have_input_arc,
&in_mapper,
&forward_local,
cch_arc_count,
);
let backward_input_arc_of_cch = expand_local_to_full(
&does_cch_arc_have_input_arc,
&in_mapper,
&backward_local,
cch_arc_count,
);
let first_extra_forward_input_arc_of_cch = expand_extra_csr(
&does_cch_arc_have_extra_input_arc,
&extra_mapper,
&first_extra_forward_local,
cch_arc_count,
)?;
let first_extra_backward_input_arc_of_cch = expand_extra_csr(
&does_cch_arc_have_extra_input_arc,
&extra_mapper,
&first_extra_backward_local,
cch_arc_count,
)?;
if first_extra_forward_input_arc_of_cch[cch_arc_count] as usize
!= extra_forward_input_arc_of_cch.len()
{
return Err(io::Error::new(
io::ErrorKind::InvalidData,
"forward extra CSR total disagrees with extra list length",
));
}
if first_extra_backward_input_arc_of_cch[cch_arc_count] as usize
!= extra_backward_input_arc_of_cch.len()
{
return Err(io::Error::new(
io::ErrorKind::InvalidData,
"backward extra CSR total disagrees with extra list length",
));
}
Ok(Cch {
rank,
order,
elimination_tree_parent,
up_first_out,
up_head,
up_tail,
down_first_out,
down_head,
down_to_up,
input_arc_to_cch_arc,
forward_input_arc_of_cch,
backward_input_arc_of_cch,
first_extra_forward_input_arc_of_cch,
extra_forward_input_arc_of_cch,
first_extra_backward_input_arc_of_cch,
extra_backward_input_arc_of_cch,
})
}
}
impl Metric {
pub fn save(&self, path: &std::path::Path) -> io::Result<()> {
let file = std::fs::File::create(path)?;
let mut out = std::io::BufWriter::new(file);
self.write_metric(&mut out)?;
out.flush()
}
fn write_metric<W: Write>(&self, out: &mut W) -> io::Result<()> {
let cch_arc_count = self.forward.len() as u64;
write_u64(out, METRIC_MAGIC)?;
write_u32(out, FORMAT_VERSION)?;
write_u32(out, 0)?; write_u64(out, cch_arc_count)?;
write_sized_vector(out, &self.forward)?;
write_sized_vector(out, &self.backward)?;
Ok(())
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::INF_WEIGHT;
use crate::graph::Graph;
fn csr(node_count: u32, tail: &[u32], head: &[u32]) -> Graph {
let n = node_count as usize;
let mut degree = vec![0u32; n];
for &t in tail {
degree[t as usize] += 1;
}
let mut first_out = vec![0u32; n + 1];
for v in 0..n {
first_out[v + 1] = first_out[v] + degree[v];
}
let mut next: Vec<u32> = first_out[..n].to_vec();
let mut g_head = vec![0u32; head.len()];
for (&t, &h) in tail.iter().zip(head.iter()) {
let slot = next[t as usize] as usize;
g_head[slot] = h;
next[t as usize] += 1;
}
Graph {
first_out,
head: g_head,
weight: vec![1u32; head.len()],
}
}
fn to_bytes(c: &Cch) -> Vec<u8> {
let dir = tempfile::tempdir().expect("tempdir");
let path = dir.path().join("tmp.cch-struct");
c.save_struct(&path).expect("save_struct");
std::fs::read(&path).expect("read back struct bytes")
}
fn assert_round_trip(name: &str, c: &Cch, weight_sets: &[Vec<u32>]) {
let bytes = to_bytes(c);
let loaded = Cch::read_struct(&bytes).expect("read_struct");
assert_eq!(loaded.rank, c.rank, "[{name}] rank");
assert_eq!(loaded.order, c.order, "[{name}] order");
assert_eq!(
loaded.elimination_tree_parent, c.elimination_tree_parent,
"[{name}] elim"
);
assert_eq!(loaded.up_first_out, c.up_first_out, "[{name}] up_first_out");
assert_eq!(loaded.up_head, c.up_head, "[{name}] up_head");
assert_eq!(loaded.up_tail, c.up_tail, "[{name}] up_tail");
assert_eq!(
loaded.down_first_out, c.down_first_out,
"[{name}] down_first_out"
);
assert_eq!(loaded.down_head, c.down_head, "[{name}] down_head");
assert_eq!(loaded.down_to_up, c.down_to_up, "[{name}] down_to_up");
assert_eq!(
loaded.input_arc_to_cch_arc, c.input_arc_to_cch_arc,
"[{name}] input_arc_to_cch_arc"
);
for (i, w) in weight_sets.iter().enumerate() {
let want = c.customize(w);
let got = loaded.customize(w);
assert_eq!(want.forward, got.forward, "[{name}] forward weights #{i}");
assert_eq!(
want.backward, got.backward,
"[{name}] backward weights #{i}"
);
}
}
#[test]
fn round_trip_path_identity() {
let n = 5u32;
let tail = vec![0u32, 1, 1, 2, 2, 3, 3, 4];
let head = vec![1u32, 0, 2, 1, 3, 2, 4, 3];
let c = Cch::build(&csr(n, &tail, &head), &(0..n).collect::<Vec<_>>());
let ws = vec![
vec![10u32, 11, 20, 21, 30, 31, 40, 41],
vec![1u32, 1, 1, 1, 1, 1, 1, 1],
vec![INF_WEIGHT, 5, INF_WEIGHT, 7, 9, INF_WEIGHT, 2, 3],
];
assert_round_trip("path_identity", &c, &ws);
}
#[test]
fn round_trip_fillin_nonidentity_order() {
let n = 4u32;
let tail = vec![0u32, 0, 0, 1, 2, 3];
let head = vec![1u32, 2, 3, 0, 0, 0];
let order = vec![0u32, 1, 2, 3];
let c = Cch::build(&csr(n, &tail, &head), &order);
let ws = vec![vec![5u32, 7, 9, 6, 8, 10], vec![1u32, 2, 3, 4, 5, 6]];
assert_round_trip("fillin", &c, &ws);
}
#[test]
fn round_trip_parallel_arcs() {
let n = 4u32;
let tail = vec![0u32, 0, 1, 1, 1, 2, 2, 3];
let head = vec![1u32, 1, 0, 0, 2, 1, 3, 2];
let order = vec![2u32, 0, 3, 1];
let c = Cch::build(&csr(n, &tail, &head), &order);
let extra_total =
c.extra_forward_input_arc_of_cch.len() + c.extra_backward_input_arc_of_cch.len();
assert!(
extra_total > 0,
"fixture must have parallel arcs in the extra lists"
);
let ws = vec![
vec![50u32, 9, 40, 8, 17, 18, 19, 20],
vec![9u32, 50, 8, 40, 1, 2, 3, 4],
];
assert_round_trip("parallel_arcs", &c, &ws);
}
#[test]
fn round_trip_grid_block_boundary() {
let cols = 24u32;
let rows = 24u32;
let n = cols * rows;
let mut tail = Vec::new();
let mut head = Vec::new();
for r in 0..rows {
for c in 0..cols {
let v = r * cols + c;
if c + 1 < cols {
tail.push(v);
head.push(v + 1);
}
if c > 0 {
tail.push(v);
head.push(v - 1);
}
if r + 1 < rows {
tail.push(v);
head.push(v + cols);
}
if r > 0 {
tail.push(v);
head.push(v - cols);
}
}
}
let graph = csr(n, &tail, &head);
let order = crate::degree_order(&graph);
let c = Cch::build(&graph, &order);
assert!(c.cch_arc_count() > 512, "grid must exceed 512 CCH arcs");
#[allow(clippy::cast_possible_truncation)]
let w: Vec<u32> = (0..tail.len() as u32)
.map(|i| (i * 7 + 1) % 9973 + 1)
.collect();
assert_round_trip("grid_24x24", &c, &[w]);
}
#[test]
fn round_trip_empty_and_single_node() {
let n = 4u32;
let c = Cch::build(&csr(n, &[], &[]), &(0..n).collect::<Vec<_>>());
assert_round_trip("empty_arcs", &c, &[vec![]]);
let c = Cch::build(&csr(1, &[], &[]), &[0]);
assert_round_trip("single_node", &c, &[vec![]]);
}
fn valid_struct_bytes() -> Vec<u8> {
let n = 4u32;
let tail = vec![0u32, 0, 1, 1, 1, 2, 2, 3];
let head = vec![1u32, 1, 0, 0, 2, 1, 3, 2];
let c = Cch::build(&csr(n, &tail, &head), &[0, 1, 2, 3]);
to_bytes(&c)
}
fn assert_invalid(bytes: &[u8], must_contain: &str) {
let err = Cch::read_struct(bytes).map(|_| ()).unwrap_err();
assert_eq!(
err.kind(),
io::ErrorKind::InvalidData,
"expected InvalidData, got: {err}"
);
assert!(
err.to_string().contains(must_contain),
"error '{err}' must contain '{must_contain}'"
);
}
#[test]
fn corrupt_baseline_is_valid() {
Cch::read_struct(&valid_struct_bytes()).expect("baseline must be valid");
}
#[test]
fn corrupt_empty_buffer_truncated() {
assert_invalid(&[], "truncated");
}
#[test]
fn corrupt_bad_magic() {
let mut b = valid_struct_bytes();
b[0..8].copy_from_slice(&0xDEAD_BEEF_DEAD_BEEFu64.to_le_bytes());
assert_invalid(&b, "bad magic");
}
#[test]
fn corrupt_bad_version() {
let mut b = valid_struct_bytes();
b[8..12].copy_from_slice(&2u32.to_le_bytes());
assert_invalid(&b, "unsupported .cch-struct version");
}
#[test]
fn corrupt_truncated_after_header() {
let b = valid_struct_bytes();
assert_invalid(&b[..40], "truncated");
}
#[test]
fn corrupt_truncated_mid_section() {
let b = valid_struct_bytes();
assert_invalid(&b[..50], "order");
}
#[test]
fn corrupt_inflated_node_count() {
let mut b = valid_struct_bytes();
let nc = {
let mut buf = [0u8; 8];
buf.copy_from_slice(&b[16..24]);
u64::from_le_bytes(buf)
};
b[16..24].copy_from_slice(&(nc + 1_000_000).to_le_bytes());
assert_invalid(&b, "does not match expected");
}
#[test]
fn corrupt_node_count_absurd() {
let mut b = valid_struct_bytes();
b[16..24].copy_from_slice(&u64::MAX.to_le_bytes());
assert_invalid(&b, "does not match expected");
}
#[test]
fn corrupt_order_byte_length_not_multiple_of_4() {
let mut b = valid_struct_bytes();
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();
crafted.extend_from_slice(&3u64.to_le_bytes());
crafted.extend_from_slice(&[0u8, 0, 0]);
assert_invalid(&crafted, "not a multiple of 4");
}
#[test]
fn corrupt_bitvector_byte_length_mismatch() {
let mut b = valid_struct_bytes();
let off = first_bitvector_byte_length_offset(&b);
b[off + 8..off + 16].copy_from_slice(&999u64.to_le_bytes());
assert_invalid(&b, "bitvector byte_length");
}
fn read_u64_usize(b: &[u8], o: usize) -> usize {
let mut buf = [0u8; 8];
buf.copy_from_slice(&b[o..o + 8]);
usize::try_from(u64::from_le_bytes(buf)).expect("offset fits usize in test")
}
fn first_bitvector_byte_length_offset(b: &[u8]) -> usize {
let mut pos = 40usize;
for _ in 0..10 {
pos += 8 + read_u64_usize(b, pos);
}
pos
}
#[test]
fn corrupt_extra_csr_total_mismatch() {
let n = 4u32;
let tail = vec![0u32, 0, 1, 1, 1, 2, 2, 3];
let head = vec![1u32, 1, 0, 0, 2, 1, 3, 2];
let c = Cch::build(&csr(n, &tail, &head), &[0, 1, 2, 3]);
assert!(
!c.extra_forward_input_arc_of_cch.is_empty(),
"need a non-empty forward extra list"
);
let mut b = to_bytes(&c);
let off = extra_forward_byte_length_offset(&b);
let cur = read_u64_usize(&b, off);
assert!(cur >= 4);
let shrunk = u64::try_from(cur - 4).expect("fits");
b[off..off + 8].copy_from_slice(&shrunk.to_le_bytes());
b.drain(off + 8 + (cur - 4)..off + 8 + cur);
assert_invalid(&b, "extra CSR total disagrees");
}
fn extra_forward_byte_length_offset(b: &[u8]) -> usize {
let mut pos = 40usize;
for _ in 0..10 {
pos += 8 + read_u64_usize(b, pos);
}
for _ in 0..3 {
pos += 16 + read_u64_usize(b, pos + 8);
}
for _ in 0..4 {
pos += 8 + read_u64_usize(b, pos);
}
pos
}
fn section_start_offsets(b: &[u8]) -> Vec<usize> {
let mut starts = Vec::with_capacity(19);
let mut pos = 40usize;
for _ in 0..10 {
starts.push(pos);
pos += 8 + read_u64_usize(b, pos);
}
for _ in 0..3 {
starts.push(pos);
pos += 16 + read_u64_usize(b, pos + 8);
}
for _ in 0..6 {
starts.push(pos);
pos += 8 + read_u64_usize(b, pos);
}
starts
}
#[test]
fn corrupt_truncate_at_each_section_start() {
let b = valid_struct_bytes();
for (i, &start) in section_start_offsets(&b).iter().enumerate() {
let truncated = &b[..start];
let err = Cch::read_struct(truncated).map(|_| ()).unwrap_err();
assert_eq!(
err.kind(),
io::ErrorKind::InvalidData,
"section #{i} truncation must be InvalidData (got {err})"
);
}
}
#[test]
fn corrupt_truncated_u32_field() {
let b = valid_struct_bytes();
assert_invalid(&b[..9], "truncated");
}
#[test]
fn corrupt_bitvector_bit_count_mismatch() {
let mut b = valid_struct_bytes();
let starts = section_start_offsets(&b);
let off = starts[11];
let real_bits = read_u64_usize(&b, off) as u64;
assert!(real_bits > 0, "fixture must have CCH arcs");
b[off..off + 8].copy_from_slice(&(real_bits + 1).to_le_bytes());
assert_invalid(&b, "does not match expected");
}
#[test]
fn corrupt_bitvector_data_truncated() {
let b = valid_struct_bytes();
let starts = section_start_offsets(&b);
let off = starts[10]; let byte_length = read_u64_usize(&b, off + 8);
assert!(byte_length > 0, "bitvector must have data to truncate");
let cut = off + 16 + 8;
assert!(cut < off + 16 + byte_length, "must truncate mid-data");
assert_invalid(&b[..cut], "is_input_arc_upward");
}
#[test]
fn corrupt_extra_csr_non_monotonic() {
let n = 4u32;
let tail = vec![0u32, 0, 1, 1, 1, 2, 2, 3];
let head = vec![1u32, 1, 0, 0, 2, 1, 3, 2];
let c = Cch::build(&csr(n, &tail, &head), &[0, 1, 2, 3]);
assert!(
!c.extra_forward_input_arc_of_cch.is_empty(),
"need a non-empty forward extra list"
);
let mut b = to_bytes(&c);
let starts = section_start_offsets(&b);
let data = starts[15] + 8;
b[data..data + 4].copy_from_slice(&0xFFFF_FFFFu32.to_le_bytes());
assert_invalid(&b, "not monotonic");
}
#[test]
fn corrupt_backward_extra_csr_non_monotonic() {
let n = 4u32;
let tail = vec![0u32, 0, 1, 1, 1, 2, 2, 3];
let head = vec![1u32, 1, 0, 0, 2, 1, 3, 2];
let c = Cch::build(&csr(n, &tail, &head), &[0, 1, 2, 3]);
assert!(
!c.extra_backward_input_arc_of_cch.is_empty(),
"need a non-empty backward extra list"
);
let mut b = to_bytes(&c);
let starts = section_start_offsets(&b);
let data = starts[16] + 8;
b[data..data + 4].copy_from_slice(&0xFFFF_FFFFu32.to_le_bytes());
assert_invalid(&b, "not monotonic");
}
#[test]
fn corrupt_backward_extra_csr_total_mismatch() {
let n = 4u32;
let tail = vec![0u32, 0, 1, 1, 1, 2, 2, 3];
let head = vec![1u32, 1, 0, 0, 2, 1, 3, 2];
let c = Cch::build(&csr(n, &tail, &head), &[0, 1, 2, 3]);
assert!(
!c.extra_backward_input_arc_of_cch.is_empty(),
"need a non-empty backward extra list"
);
let mut b = to_bytes(&c);
let starts = section_start_offsets(&b);
let off = starts[18];
let cur = read_u64_usize(&b, off);
assert!(cur >= 4);
let shrunk = u64::try_from(cur - 4).expect("fits");
b[off..off + 8].copy_from_slice(&shrunk.to_le_bytes());
b.drain(off + 8 + (cur - 4)..off + 8 + cur);
assert_invalid(&b, "backward extra CSR total disagrees");
}
}