use std::array;
#[derive(Debug, Clone)]
pub struct ReadyGop {
pub gop_idx: usize,
pub plans: [Vec<u8>; 4],
}
pub struct PerGopPlanBuilder {
cum_counts: [Vec<usize>; 4],
gop_buffers: Vec<[Option<Vec<u8>>; 4]>,
gop_filled: Vec<[usize; 4]>,
gop_domain_done: Vec<[bool; 4]>,
gop_fired: Vec<bool>,
num_gops: usize,
total_modifications: usize,
total_cost: f64,
}
impl PerGopPlanBuilder {
pub fn new(
per_gop_counts: &[[usize; 4]],
active_domains: [bool; 4],
) -> Self {
let num_gops = per_gop_counts.len();
let cum_counts: [Vec<usize>; 4] = array::from_fn(|d| {
let mut v = Vec::with_capacity(num_gops + 1);
v.push(0);
for row in per_gop_counts.iter() {
v.push(*v.last().unwrap() + row[d]);
}
v
});
let gop_buffers: Vec<[Option<Vec<u8>>; 4]> =
(0..num_gops).map(|_| [None, None, None, None]).collect();
let gop_filled = vec![[0usize; 4]; num_gops];
let gop_domain_done: Vec<[bool; 4]> = (0..num_gops)
.map(|g| {
array::from_fn(|d| {
!active_domains[d] || per_gop_counts[g][d] == 0
})
})
.collect();
let gop_fired = vec![false; num_gops];
Self {
cum_counts,
gop_buffers,
gop_filled,
gop_domain_done,
gop_fired,
num_gops,
total_modifications: 0,
total_cost: 0.0,
}
}
pub fn total_positions(&self, domain_idx: usize) -> usize {
*self.cum_counts[domain_idx].last().unwrap_or(&0)
}
pub fn num_gops(&self) -> usize {
self.num_gops
}
pub fn total_modifications(&self) -> usize {
self.total_modifications
}
pub fn total_cost(&self) -> f64 {
self.total_cost
}
pub fn add_modifications(&mut self, n: usize) {
self.total_modifications += n;
}
pub fn add_cost(&mut self, c: f64) {
self.total_cost += c;
}
pub fn all_fired(&self) -> bool {
self.gop_fired.iter().all(|&f| f)
}
pub fn highest_pending_gop(&self, domain_idx: usize) -> Option<usize> {
if domain_idx >= 4 {
return None;
}
for g in (0..self.num_gops).rev() {
if !self.gop_domain_done[g][domain_idx] {
return Some(g);
}
}
None
}
pub fn accept_emission(
&mut self,
domain_idx: usize,
j_start: usize,
stego_bits: &[u8],
) -> Result<(), &'static str> {
if domain_idx >= 4 {
return Err("domain_idx out of range");
}
if stego_bits.is_empty() {
return Ok(());
}
let j_end = j_start + stego_bits.len();
let total = self.total_positions(domain_idx);
if j_end > total {
return Err("emission overruns cumulative count");
}
let cum = &self.cum_counts[domain_idx];
let gop_start = cum.partition_point(|&c| c <= j_start).saturating_sub(1);
let gop_end = cum.partition_point(|&c| c < j_end);
let mut bits_offset = 0usize;
for g in gop_start..gop_end {
let gop_lo = cum[g];
let gop_hi = cum[g + 1];
let copy_lo = j_start.max(gop_lo);
let copy_hi = j_end.min(gop_hi);
if copy_hi <= copy_lo {
continue;
}
let copy_len = copy_hi - copy_lo;
let target_lo = copy_lo - gop_lo;
let target_hi = target_lo + copy_len;
if self.gop_fired[g] {
return Err("GOP buffer already fired");
}
let expected = gop_hi - gop_lo;
let buf = self.gop_buffers[g][domain_idx]
.get_or_insert_with(|| vec![0u8; expected]);
buf[target_lo..target_hi]
.copy_from_slice(&stego_bits[bits_offset..bits_offset + copy_len]);
self.gop_filled[g][domain_idx] += copy_len;
if self.gop_filled[g][domain_idx] >= expected {
self.gop_domain_done[g][domain_idx] = true;
}
bits_offset += copy_len;
}
if bits_offset != stego_bits.len() {
return Err("emission span calc mismatch");
}
Ok(())
}
pub fn take_ready_gops(&mut self) -> Vec<ReadyGop> {
let mut out = Vec::new();
for g in 0..self.num_gops {
if self.gop_fired[g] {
continue;
}
if !self.gop_domain_done[g].iter().all(|&b| b) {
continue;
}
let plans: [Vec<u8>; 4] = array::from_fn(|d| {
self.gop_buffers[g][d].take().unwrap_or_default()
});
self.gop_fired[g] = true;
out.push(ReadyGop { gop_idx: g, plans });
}
out
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn single_domain_single_gop_fires_immediately() {
let counts = vec![[8, 0, 0, 0]];
let mut b = PerGopPlanBuilder::new(&counts, [true, false, false, false]);
assert_eq!(b.num_gops(), 1);
assert_eq!(b.total_positions(0), 8);
assert!(b.take_ready_gops().is_empty());
let bits: Vec<u8> = (0..8u8).map(|i| i & 1).collect();
b.accept_emission(0, 0, &bits).unwrap();
let ready = b.take_ready_gops();
assert_eq!(ready.len(), 1);
assert_eq!(ready[0].gop_idx, 0);
assert_eq!(ready[0].plans[0], bits);
assert!(ready[0].plans[1].is_empty());
assert!(ready[0].plans[2].is_empty());
assert!(ready[0].plans[3].is_empty());
assert!(b.all_fired());
}
#[test]
fn cross_gop_segment_routes_to_both_gops() {
let counts = vec![[5, 0, 0, 0], [5, 0, 0, 0]];
let mut b =
PerGopPlanBuilder::new(&counts, [true, false, false, false]);
let bits: Vec<u8> = vec![1, 0, 1, 1, 0];
b.accept_emission(0, 3, &bits).unwrap();
assert!(b.take_ready_gops().is_empty());
b.accept_emission(0, 0, &[0, 1, 0]).unwrap();
let ready = b.take_ready_gops();
assert_eq!(ready.len(), 1);
assert_eq!(ready[0].gop_idx, 0);
assert_eq!(ready[0].plans[0], vec![0, 1, 0, 1, 0]);
b.accept_emission(0, 8, &[1, 1]).unwrap();
let ready = b.take_ready_gops();
assert_eq!(ready.len(), 1);
assert_eq!(ready[0].gop_idx, 1);
assert_eq!(ready[0].plans[0], vec![1, 1, 0, 1, 1]);
assert!(b.all_fired());
}
#[test]
fn four_domain_gop_waits_for_all_four() {
let counts = vec![[4, 4, 4, 4]];
let mut b = PerGopPlanBuilder::new(&counts, [true; 4]);
b.accept_emission(0, 0, &[0, 1, 0, 1]).unwrap();
assert!(b.take_ready_gops().is_empty());
b.accept_emission(1, 0, &[1, 0, 1, 0]).unwrap();
assert!(b.take_ready_gops().is_empty());
b.accept_emission(2, 0, &[1, 1, 0, 0]).unwrap();
assert!(b.take_ready_gops().is_empty());
b.accept_emission(3, 0, &[0, 0, 1, 1]).unwrap();
let ready = b.take_ready_gops();
assert_eq!(ready.len(), 1);
assert_eq!(ready[0].gop_idx, 0);
assert_eq!(ready[0].plans[0], vec![0, 1, 0, 1]);
assert_eq!(ready[0].plans[1], vec![1, 0, 1, 0]);
assert_eq!(ready[0].plans[2], vec![1, 1, 0, 0]);
assert_eq!(ready[0].plans[3], vec![0, 0, 1, 1]);
}
#[test]
fn inactive_domain_auto_completes() {
let counts = vec![[4, 0, 4, 0]];
let mut b = PerGopPlanBuilder::new(
&counts,
[true, false, true, false],
);
b.accept_emission(0, 0, &[1, 1, 0, 0]).unwrap();
assert!(b.take_ready_gops().is_empty());
b.accept_emission(2, 0, &[0, 1, 0, 1]).unwrap();
let ready = b.take_ready_gops();
assert_eq!(ready.len(), 1);
assert_eq!(ready[0].plans[0], vec![1, 1, 0, 0]);
assert!(ready[0].plans[1].is_empty());
assert_eq!(ready[0].plans[2], vec![0, 1, 0, 1]);
assert!(ready[0].plans[3].is_empty());
}
#[test]
fn reverse_order_gops_fire_in_reverse() {
let counts = vec![[3, 0, 0, 0], [3, 0, 0, 0], [3, 0, 0, 0]];
let mut b =
PerGopPlanBuilder::new(&counts, [true, false, false, false]);
b.accept_emission(0, 6, &[1, 0, 1]).unwrap();
let ready = b.take_ready_gops();
assert_eq!(ready.len(), 1);
assert_eq!(ready[0].gop_idx, 2);
assert_eq!(ready[0].plans[0], vec![1, 0, 1]);
b.accept_emission(0, 3, &[0, 1, 0]).unwrap();
let ready = b.take_ready_gops();
assert_eq!(ready.len(), 1);
assert_eq!(ready[0].gop_idx, 1);
b.accept_emission(0, 0, &[1, 1, 1]).unwrap();
let ready = b.take_ready_gops();
assert_eq!(ready.len(), 1);
assert_eq!(ready[0].gop_idx, 0);
assert!(b.all_fired());
}
#[test]
fn partial_then_completing_emission_fires_gop() {
let counts = vec![[5, 0, 0, 0]];
let mut b =
PerGopPlanBuilder::new(&counts, [true, false, false, false]);
b.accept_emission(0, 0, &[1, 0, 1]).unwrap();
assert!(b.take_ready_gops().is_empty());
b.accept_emission(0, 3, &[0, 1]).unwrap();
let ready = b.take_ready_gops();
assert_eq!(ready.len(), 1);
assert_eq!(ready[0].plans[0], vec![1, 0, 1, 0, 1]);
}
#[test]
fn empty_emission_is_noop() {
let counts = vec![[0, 0, 0, 0]];
let mut b = PerGopPlanBuilder::new(&counts, [true; 4]);
b.accept_emission(0, 0, &[]).unwrap();
let ready = b.take_ready_gops();
assert_eq!(ready.len(), 1);
assert!(ready[0].plans.iter().all(|p| p.is_empty()));
}
#[test]
fn overrun_returns_err() {
let counts = vec![[4, 0, 0, 0]];
let mut b =
PerGopPlanBuilder::new(&counts, [true, false, false, false]);
let bits = vec![0u8; 5];
assert!(b.accept_emission(0, 0, &bits).is_err());
}
#[test]
fn write_after_fire_returns_err() {
let counts = vec![[2, 0, 0, 0]];
let mut b =
PerGopPlanBuilder::new(&counts, [true, false, false, false]);
b.accept_emission(0, 0, &[1, 1]).unwrap();
let _ready = b.take_ready_gops();
assert!(b.accept_emission(0, 0, &[0, 0]).is_err());
}
#[test]
fn pass_through_totals_accumulate() {
let counts = vec![[2, 0, 0, 0]];
let mut b =
PerGopPlanBuilder::new(&counts, [true, false, false, false]);
b.add_modifications(7);
b.add_modifications(3);
b.add_cost(1.5);
b.add_cost(2.5);
assert_eq!(b.total_modifications(), 10);
assert!((b.total_cost() - 4.0).abs() < 1e-9);
}
#[test]
fn realistic_lockstep_pattern() {
let counts = vec![
[10, 10, 10, 10],
[8, 8, 8, 8],
[6, 6, 6, 6],
[4, 4, 4, 4],
];
let mut b = PerGopPlanBuilder::new(&counts, [true; 4]);
let total_per_domain = 10 + 8 + 6 + 4;
let seg_size = 7;
for d in 0..4 {
for seg_idx_rev in (0..4).rev() {
let j = seg_idx_rev * seg_size;
let bits: Vec<u8> = (0..seg_size)
.map(|i| ((d * 100 + j + i) & 1) as u8)
.collect();
b.accept_emission(d, j, &bits).unwrap();
}
}
let mut all_ready = Vec::new();
loop {
let r = b.take_ready_gops();
if r.is_empty() {
break;
}
all_ready.extend(r);
}
assert_eq!(all_ready.len(), 4);
for (i, rg) in all_ready.iter().enumerate() {
assert_eq!(rg.gop_idx, i);
for d in 0..4 {
let expected_len = counts[i][d];
assert_eq!(
rg.plans[d].len(),
expected_len,
"GOP {i} domain {d} length mismatch",
);
}
}
assert!(b.all_fired());
}
}