use crate::util::hex_encode;
use serde::{Deserialize, Serialize};
use sha2::{Digest, Sha256};
use std::{
io::{self, BufRead, Read, Seek, SeekFrom, Write},
sync::mpsc,
thread,
};
pub const JSONL_WITNESS_SCHEMA_VERSION: &str = "br.jsonl-witness.v1";
const ROOT_DOMAIN: &[u8] = b"br:jsonl-witness:root:v1\0";
const CHUNK_DOMAIN: &[u8] = b"br:jsonl-witness:chunk:v1\0";
const FIELD_SEPARATOR: &[u8] = b"\0";
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct JsonlMerkleWitness {
pub schema_version: String,
pub chunk_size_lines: usize,
pub line_count: usize,
pub byte_count: u64,
pub root_hash: String,
pub chunks: Vec<JsonlChunkWitness>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct JsonlChunkWitness {
pub index: usize,
pub start_line: usize,
pub line_count: usize,
pub byte_count: u64,
pub hash: String,
pub first_line_hash: Option<String>,
pub last_line_hash: Option<String>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[must_use = "comparison summaries identify which chunks can be reused safely"]
pub struct JsonlWitnessComparison {
pub schema_versions_match: bool,
pub chunk_size_lines_match: bool,
pub root_hashes_match: bool,
pub drift_detected: bool,
pub base_line_count: usize,
pub candidate_line_count: usize,
pub base_byte_count: u64,
pub candidate_byte_count: u64,
pub base_chunk_count: usize,
pub candidate_chunk_count: usize,
pub comparable_chunk_count: usize,
pub unchanged_chunks: usize,
pub changed_chunks: usize,
pub added_chunks: usize,
pub removed_chunks: usize,
pub unchanged_byte_count: u64,
pub changed_base_byte_count: u64,
pub changed_candidate_byte_count: u64,
pub added_byte_count: u64,
pub removed_byte_count: u64,
pub safe_reuse_prefix_chunks: usize,
pub first_changed_chunk_index: Option<usize>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[must_use = "reuse plans identify which chunks can be copied or rebuilt"]
pub struct JsonlWitnessReusePlan {
pub comparison: JsonlWitnessComparison,
pub schedule: JsonlWitnessReuseSchedule,
pub actions: Vec<JsonlChunkReuseStep>,
}
#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
#[must_use = "materialization summaries prove how many chunks were reused or rebuilt"]
pub struct JsonlWitnessReuseMaterialization {
pub reused_chunks: usize,
pub rebuilt_chunks: usize,
pub read_added_chunks: usize,
pub dropped_chunks: usize,
pub output_byte_count: u64,
pub reused_byte_count: u64,
pub rebuilt_byte_count: u64,
pub read_added_byte_count: u64,
pub dropped_byte_count: u64,
}
impl JsonlWitnessReuseMaterialization {
fn record_reused(&mut self, byte_count: u64) -> io::Result<()> {
self.reused_chunks += 1;
self.reused_byte_count =
checked_add_u64(self.reused_byte_count, byte_count, "reused byte count")?;
self.record_output_bytes(byte_count)
}
fn record_rebuilt(&mut self, byte_count: u64) -> io::Result<()> {
self.rebuilt_chunks += 1;
self.rebuilt_byte_count =
checked_add_u64(self.rebuilt_byte_count, byte_count, "rebuilt byte count")?;
self.record_output_bytes(byte_count)
}
fn record_read_added(&mut self, byte_count: u64) -> io::Result<()> {
self.read_added_chunks += 1;
self.read_added_byte_count = checked_add_u64(
self.read_added_byte_count,
byte_count,
"read-added byte count",
)?;
self.record_output_bytes(byte_count)
}
fn record_dropped(&mut self, byte_count: u64) -> io::Result<()> {
self.dropped_chunks += 1;
self.dropped_byte_count =
checked_add_u64(self.dropped_byte_count, byte_count, "dropped byte count")?;
Ok(())
}
fn record_output_bytes(&mut self, byte_count: u64) -> io::Result<()> {
self.output_byte_count = checked_add_u64(
self.output_byte_count,
byte_count,
"materialized output byte count",
)?;
Ok(())
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct JsonlWitnessParallelWorkPlan {
pub max_parallelism: usize,
pub total_batches: usize,
pub candidate_output_batches: usize,
pub metadata_only_drop_batches: usize,
pub deterministic_batch_order: bool,
pub batches: Vec<JsonlWitnessWorkBatch>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct JsonlWitnessWorkBatch {
pub index: usize,
pub kind: JsonlWitnessWorkBatchKind,
pub action_count: usize,
pub candidate_start_index: Option<usize>,
pub candidate_end_index: Option<usize>,
pub line_count: usize,
pub byte_count: u64,
pub actions: Vec<JsonlChunkReuseStep>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum JsonlWitnessWorkBatchKind {
CandidateOutput,
MetadataOnlyDrop,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct JsonlWitnessReuseSchedule {
pub total_actions: usize,
pub candidate_output_actions: usize,
pub metadata_only_drop_actions: usize,
pub reusable_actions: usize,
pub rebuild_actions: usize,
pub read_added_actions: usize,
pub candidate_output_line_count: usize,
pub candidate_output_byte_count: u64,
pub reusable_byte_count: u64,
pub rebuild_byte_count: u64,
pub read_added_byte_count: u64,
pub dropped_byte_count: u64,
pub max_parallel_candidate_actions: usize,
pub deterministic_candidate_order: bool,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct JsonlChunkReuseStep {
pub action: JsonlChunkReuseAction,
pub base_index: Option<usize>,
pub candidate_index: Option<usize>,
pub start_line: usize,
pub line_count: usize,
pub byte_count: u64,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum JsonlChunkReuseAction {
ReuseUnchanged,
RebuildCandidate,
ReadAdded,
DropRemoved,
}
pub fn build_jsonl_merkle_witness<R: BufRead>(
mut reader: R,
chunk_size_lines: usize,
) -> io::Result<JsonlMerkleWitness> {
if chunk_size_lines == 0 {
return Err(io::Error::new(
io::ErrorKind::InvalidInput,
"chunk_size_lines must be greater than zero",
));
}
let mut chunks = Vec::new();
let mut line_count = 0_usize;
let mut byte_count = 0_u64;
let mut chunk_builder = ChunkBuilder::new(0, 0)?;
let mut line = Vec::new();
loop {
line.clear();
let bytes_read = reader.read_until(b'\n', &mut line)?;
if bytes_read == 0 {
break;
}
if chunk_builder.line_count == chunk_size_lines {
chunks.push(chunk_builder.finish());
chunk_builder = ChunkBuilder::new(chunks.len(), line_count)?;
}
line_count = line_count
.checked_add(1)
.ok_or_else(|| io_invalid_data("JSONL line count overflowed usize"))?;
byte_count = checked_add_bytes(byte_count, bytes_read, "JSONL byte count")?;
chunk_builder.push_line(&line, bytes_read)?;
}
if chunk_builder.line_count > 0 {
chunks.push(chunk_builder.finish());
}
let root_hash = compute_root_hash(chunk_size_lines, line_count, byte_count, &chunks)?;
Ok(JsonlMerkleWitness {
schema_version: JSONL_WITNESS_SCHEMA_VERSION.to_string(),
chunk_size_lines,
line_count,
byte_count,
root_hash,
chunks,
})
}
pub fn build_jsonl_merkle_witness_parallel<R: BufRead>(
reader: R,
chunk_size_lines: usize,
max_parallelism: usize,
) -> io::Result<JsonlMerkleWitness> {
if max_parallelism == 0 {
return Err(io::Error::new(
io::ErrorKind::InvalidInput,
"max_parallelism must be greater than zero",
));
}
if max_parallelism == 1 {
return build_jsonl_merkle_witness(reader, chunk_size_lines);
}
if chunk_size_lines == 0 {
return Err(io::Error::new(
io::ErrorKind::InvalidInput,
"chunk_size_lines must be greater than zero",
));
}
let (chunks, line_count, byte_count) =
build_parallel_chunks(reader, chunk_size_lines, max_parallelism)?;
let root_hash = compute_root_hash(chunk_size_lines, line_count, byte_count, &chunks)?;
Ok(JsonlMerkleWitness {
schema_version: JSONL_WITNESS_SCHEMA_VERSION.to_string(),
chunk_size_lines,
line_count,
byte_count,
root_hash,
chunks,
})
}
pub fn plan_jsonl_witness_reuse(
base: &JsonlMerkleWitness,
candidate: &JsonlMerkleWitness,
) -> JsonlWitnessReusePlan {
let comparison = compare_jsonl_merkle_witnesses(base, candidate);
let witnesses_are_comparable =
comparison.schema_versions_match && comparison.chunk_size_lines_match;
let comparable_chunk_count = if witnesses_are_comparable {
base.chunks.len().min(candidate.chunks.len())
} else {
0
};
let mut actions = Vec::with_capacity(base.chunks.len().max(candidate.chunks.len()));
if witnesses_are_comparable {
for (index, (base_chunk, candidate_chunk)) in
base.chunks.iter().zip(&candidate.chunks).enumerate()
{
let action = if chunks_match_for_reuse(base_chunk, candidate_chunk) {
JsonlChunkReuseAction::ReuseUnchanged
} else {
JsonlChunkReuseAction::RebuildCandidate
};
actions.push(candidate_chunk_step(
action,
Some(index),
index,
candidate_chunk,
));
}
}
for (offset, chunk) in candidate
.chunks
.get(comparable_chunk_count..)
.unwrap_or(&[])
.iter()
.enumerate()
{
actions.push(candidate_chunk_step(
if witnesses_are_comparable {
JsonlChunkReuseAction::ReadAdded
} else {
JsonlChunkReuseAction::RebuildCandidate
},
None,
comparable_chunk_count + offset,
chunk,
));
}
for (offset, chunk) in base
.chunks
.get(comparable_chunk_count..)
.unwrap_or(&[])
.iter()
.enumerate()
{
actions.push(base_chunk_step(
JsonlChunkReuseAction::DropRemoved,
comparable_chunk_count + offset,
chunk,
));
}
let schedule = build_reuse_schedule(&actions);
JsonlWitnessReusePlan {
comparison,
schedule,
actions,
}
}
pub fn plan_jsonl_witness_parallel_work(
plan: &JsonlWitnessReusePlan,
max_parallelism: usize,
) -> io::Result<JsonlWitnessParallelWorkPlan> {
if max_parallelism == 0 {
return Err(io::Error::new(
io::ErrorKind::InvalidInput,
"max_parallelism must be greater than zero",
));
}
let mut batches = Vec::new();
let mut current_kind = None;
let mut current_actions = Vec::new();
for step in &plan.actions {
let step_kind = work_batch_kind(step);
if let Some(kind) = current_kind {
if kind != step_kind || current_actions.len() == max_parallelism {
push_work_batch(&mut batches, kind, std::mem::take(&mut current_actions));
current_kind = Some(step_kind);
}
} else {
current_kind = Some(step_kind);
}
current_actions.push(step.clone());
}
if let Some(kind) = current_kind {
push_work_batch(&mut batches, kind, current_actions);
}
let candidate_output_batches = batches
.iter()
.filter(|batch| matches!(batch.kind, JsonlWitnessWorkBatchKind::CandidateOutput))
.count();
let metadata_only_drop_batches = batches
.iter()
.filter(|batch| matches!(batch.kind, JsonlWitnessWorkBatchKind::MetadataOnlyDrop))
.count();
Ok(JsonlWitnessParallelWorkPlan {
max_parallelism,
total_batches: batches.len(),
candidate_output_batches,
metadata_only_drop_batches,
deterministic_batch_order: plan.schedule.deterministic_candidate_order
&& work_batches_are_deterministically_ordered(&batches),
batches,
})
}
pub fn materialize_jsonl_witness_reuse_plan<RB, RC, W>(
base_reader: &mut RB,
candidate_reader: &mut RC,
writer: &mut W,
base_witness: &JsonlMerkleWitness,
candidate_witness: &JsonlMerkleWitness,
plan: &JsonlWitnessReusePlan,
) -> io::Result<JsonlWitnessReuseMaterialization>
where
RB: Read + Seek,
RC: Read + Seek,
W: Write,
{
validate_reuse_materialization_inputs(base_witness, candidate_witness, plan)?;
let base_offsets = chunk_start_offsets(base_witness, "base")?;
let candidate_offsets = chunk_start_offsets(candidate_witness, "candidate")?;
let mut materialization = JsonlWitnessReuseMaterialization::default();
for step in &plan.actions {
match step.action {
JsonlChunkReuseAction::ReuseUnchanged => {
let base_index = required_chunk_index(step.base_index, "reuse base_index")?;
let candidate_index =
required_chunk_index(step.candidate_index, "reuse candidate_index")?;
let base_chunk = witness_chunk(&base_witness.chunks, base_index, "base")?;
let candidate_chunk =
witness_chunk(&candidate_witness.chunks, candidate_index, "candidate")?;
validate_step_matches_chunk(step, candidate_chunk, "candidate")?;
if !chunks_match_for_reuse(base_chunk, candidate_chunk) {
return Err(io_invalid_data(
"reuse step no longer matches base and candidate chunk witnesses",
));
}
let copied = copy_witness_chunk(
base_reader,
&base_offsets,
&base_witness.chunks,
base_index,
writer,
"base",
)?;
materialization.record_reused(copied)?;
}
JsonlChunkReuseAction::RebuildCandidate => {
let candidate_index =
required_chunk_index(step.candidate_index, "rebuild candidate_index")?;
let candidate_chunk =
witness_chunk(&candidate_witness.chunks, candidate_index, "candidate")?;
validate_step_matches_chunk(step, candidate_chunk, "candidate")?;
let copied = copy_witness_chunk(
candidate_reader,
&candidate_offsets,
&candidate_witness.chunks,
candidate_index,
writer,
"candidate",
)?;
materialization.record_rebuilt(copied)?;
}
JsonlChunkReuseAction::ReadAdded => {
let candidate_index =
required_chunk_index(step.candidate_index, "read-added candidate_index")?;
if step.base_index.is_some() {
return Err(io_invalid_data(
"read-added step unexpectedly referenced a base chunk",
));
}
let candidate_chunk =
witness_chunk(&candidate_witness.chunks, candidate_index, "candidate")?;
validate_step_matches_chunk(step, candidate_chunk, "candidate")?;
let copied = copy_witness_chunk(
candidate_reader,
&candidate_offsets,
&candidate_witness.chunks,
candidate_index,
writer,
"candidate",
)?;
materialization.record_read_added(copied)?;
}
JsonlChunkReuseAction::DropRemoved => {
let base_index = required_chunk_index(step.base_index, "drop base_index")?;
if step.candidate_index.is_some() {
return Err(io_invalid_data(
"drop step unexpectedly referenced a candidate chunk",
));
}
let base_chunk = witness_chunk(&base_witness.chunks, base_index, "base")?;
validate_step_matches_chunk(step, base_chunk, "base")?;
materialization.record_dropped(step.byte_count)?;
}
}
}
if materialization.output_byte_count != candidate_witness.byte_count {
return Err(io_invalid_data(format!(
"materialized output byte count {} did not match candidate witness byte count {}",
materialization.output_byte_count, candidate_witness.byte_count
)));
}
Ok(materialization)
}
pub fn compare_jsonl_merkle_witnesses(
base: &JsonlMerkleWitness,
candidate: &JsonlMerkleWitness,
) -> JsonlWitnessComparison {
let schema_versions_match = base.schema_version == candidate.schema_version;
let chunk_size_lines_match = base.chunk_size_lines == candidate.chunk_size_lines;
let root_hashes_match = base.root_hash == candidate.root_hash;
let witnesses_are_comparable = schema_versions_match && chunk_size_lines_match;
let mut unchanged_chunks = 0;
let mut changed_chunks = 0;
let mut unchanged_byte_count = 0;
let mut changed_base_byte_count = 0;
let mut changed_candidate_byte_count = 0;
let mut safe_reuse_prefix_chunks = 0;
let mut first_changed_chunk_index = None;
let comparable_chunk_count = if witnesses_are_comparable {
let comparable_chunk_count = base.chunks.len().min(candidate.chunks.len());
let mut prefix_is_reusable = true;
for (index, (base_chunk, candidate_chunk)) in
base.chunks.iter().zip(&candidate.chunks).enumerate()
{
if chunks_match_for_reuse(base_chunk, candidate_chunk) {
unchanged_chunks += 1;
unchanged_byte_count += candidate_chunk.byte_count;
if prefix_is_reusable {
safe_reuse_prefix_chunks += 1;
}
} else {
changed_chunks += 1;
changed_base_byte_count += base_chunk.byte_count;
changed_candidate_byte_count += candidate_chunk.byte_count;
prefix_is_reusable = false;
first_changed_chunk_index.get_or_insert(index);
}
}
comparable_chunk_count
} else {
0
};
let (added_chunks, removed_chunks, added_byte_count, removed_byte_count) =
if witnesses_are_comparable {
(
candidate.chunks.len().saturating_sub(base.chunks.len()),
base.chunks.len().saturating_sub(candidate.chunks.len()),
sum_chunk_bytes(
candidate
.chunks
.get(comparable_chunk_count..)
.unwrap_or(&[]),
),
sum_chunk_bytes(base.chunks.get(comparable_chunk_count..).unwrap_or(&[])),
)
} else {
(
candidate.chunks.len(),
base.chunks.len(),
sum_chunk_bytes(&candidate.chunks),
sum_chunk_bytes(&base.chunks),
)
};
if first_changed_chunk_index.is_none()
&& (!witnesses_are_comparable || added_chunks > 0 || removed_chunks > 0)
{
first_changed_chunk_index = Some(comparable_chunk_count);
}
JsonlWitnessComparison {
schema_versions_match,
chunk_size_lines_match,
root_hashes_match,
drift_detected: !(witnesses_are_comparable && root_hashes_match),
base_line_count: base.line_count,
candidate_line_count: candidate.line_count,
base_byte_count: base.byte_count,
candidate_byte_count: candidate.byte_count,
base_chunk_count: base.chunks.len(),
candidate_chunk_count: candidate.chunks.len(),
comparable_chunk_count,
unchanged_chunks,
changed_chunks,
added_chunks,
removed_chunks,
unchanged_byte_count,
changed_base_byte_count,
changed_candidate_byte_count,
added_byte_count,
removed_byte_count,
safe_reuse_prefix_chunks,
first_changed_chunk_index,
}
}
fn chunks_match_for_reuse(base: &JsonlChunkWitness, candidate: &JsonlChunkWitness) -> bool {
base.index == candidate.index
&& base.start_line == candidate.start_line
&& base.line_count == candidate.line_count
&& base.byte_count == candidate.byte_count
&& base.hash == candidate.hash
}
fn validate_reuse_materialization_inputs(
base_witness: &JsonlMerkleWitness,
candidate_witness: &JsonlMerkleWitness,
plan: &JsonlWitnessReusePlan,
) -> io::Result<()> {
validate_chunk_sequence(base_witness)?;
validate_chunk_sequence(candidate_witness)?;
let expected_comparison = compare_jsonl_merkle_witnesses(base_witness, candidate_witness);
if plan.comparison != expected_comparison {
return Err(io_invalid_data(
"reuse plan comparison did not match supplied witnesses",
));
}
let expected_schedule = build_reuse_schedule(&plan.actions);
if plan.schedule != expected_schedule {
return Err(io_invalid_data(
"reuse plan schedule did not match supplied actions",
));
}
if !candidate_actions_are_deterministically_ordered(&plan.actions) {
return Err(io_invalid_data(
"reuse plan candidate-output actions were not in deterministic candidate order",
));
}
if plan.schedule.candidate_output_actions != candidate_witness.chunks.len() {
return Err(io_invalid_data(format!(
"reuse plan had {} candidate-output actions for {} candidate chunks",
plan.schedule.candidate_output_actions,
candidate_witness.chunks.len()
)));
}
Ok(())
}
fn validate_chunk_sequence(witness: &JsonlMerkleWitness) -> io::Result<()> {
for (expected_index, chunk) in witness.chunks.iter().enumerate() {
if chunk.index != expected_index {
return Err(io_invalid_data(
"witness chunks were not stored in ascending chunk-index order",
));
}
}
Ok(())
}
fn chunk_start_offsets(witness: &JsonlMerkleWitness, label: &'static str) -> io::Result<Vec<u64>> {
let mut offsets = Vec::with_capacity(witness.chunks.len());
let mut next_offset = 0;
for chunk in &witness.chunks {
offsets.push(next_offset);
next_offset = checked_add_u64(next_offset, chunk.byte_count, "witness chunk offset")?;
}
if next_offset != witness.byte_count {
return Err(io_invalid_data(format!(
"{label} witness chunk byte counts summed to {next_offset}, expected {}",
witness.byte_count
)));
}
Ok(offsets)
}
fn required_chunk_index(index: Option<usize>, label: &'static str) -> io::Result<usize> {
index.ok_or_else(|| io_invalid_data(format!("reuse plan step was missing {label}")))
}
fn witness_chunk<'a>(
chunks: &'a [JsonlChunkWitness],
index: usize,
label: &'static str,
) -> io::Result<&'a JsonlChunkWitness> {
chunks.get(index).ok_or_else(|| {
io_invalid_data(format!(
"{label} witness chunk index {index} was out of range"
))
})
}
fn validate_step_matches_chunk(
step: &JsonlChunkReuseStep,
chunk: &JsonlChunkWitness,
label: &'static str,
) -> io::Result<()> {
if step.start_line != chunk.start_line
|| step.line_count != chunk.line_count
|| step.byte_count != chunk.byte_count
{
return Err(io_invalid_data(format!(
"reuse plan step metadata did not match {label} witness chunk {}",
chunk.index
)));
}
Ok(())
}
fn copy_witness_chunk<R: Read + Seek, W: Write>(
reader: &mut R,
offsets: &[u64],
chunks: &[JsonlChunkWitness],
index: usize,
writer: &mut W,
label: &'static str,
) -> io::Result<u64> {
let chunk = witness_chunk(chunks, index, label)?;
let offset = offsets.get(index).ok_or_else(|| {
io_invalid_data(format!("{label} witness chunk offset {index} was missing"))
})?;
reader.seek(SeekFrom::Start(*offset))?;
let mut limited = reader.take(chunk.byte_count);
let copied = io::copy(&mut limited, writer)?;
if copied != chunk.byte_count {
return Err(io::Error::new(
io::ErrorKind::UnexpectedEof,
format!(
"{label} JSONL ended while copying chunk {index}: copied {copied} of {} bytes",
chunk.byte_count
),
));
}
Ok(copied)
}
fn candidate_chunk_step(
action: JsonlChunkReuseAction,
base_index: Option<usize>,
candidate_index: usize,
chunk: &JsonlChunkWitness,
) -> JsonlChunkReuseStep {
JsonlChunkReuseStep {
action,
base_index,
candidate_index: Some(candidate_index),
start_line: chunk.start_line,
line_count: chunk.line_count,
byte_count: chunk.byte_count,
}
}
fn base_chunk_step(
action: JsonlChunkReuseAction,
base_index: usize,
chunk: &JsonlChunkWitness,
) -> JsonlChunkReuseStep {
JsonlChunkReuseStep {
action,
base_index: Some(base_index),
candidate_index: None,
start_line: chunk.start_line,
line_count: chunk.line_count,
byte_count: chunk.byte_count,
}
}
fn build_reuse_schedule(actions: &[JsonlChunkReuseStep]) -> JsonlWitnessReuseSchedule {
let mut schedule = JsonlWitnessReuseSchedule {
total_actions: actions.len(),
candidate_output_actions: 0,
metadata_only_drop_actions: 0,
reusable_actions: 0,
rebuild_actions: 0,
read_added_actions: 0,
candidate_output_line_count: 0,
candidate_output_byte_count: 0,
reusable_byte_count: 0,
rebuild_byte_count: 0,
read_added_byte_count: 0,
dropped_byte_count: 0,
max_parallel_candidate_actions: 0,
deterministic_candidate_order: candidate_actions_are_deterministically_ordered(actions),
};
for step in actions {
match step.action {
JsonlChunkReuseAction::ReuseUnchanged => {
schedule.reusable_actions += 1;
schedule.reusable_byte_count =
schedule.reusable_byte_count.saturating_add(step.byte_count);
record_candidate_output(&mut schedule, step);
}
JsonlChunkReuseAction::RebuildCandidate => {
schedule.rebuild_actions += 1;
schedule.rebuild_byte_count =
schedule.rebuild_byte_count.saturating_add(step.byte_count);
record_candidate_output(&mut schedule, step);
}
JsonlChunkReuseAction::ReadAdded => {
schedule.read_added_actions += 1;
schedule.read_added_byte_count = schedule
.read_added_byte_count
.saturating_add(step.byte_count);
record_candidate_output(&mut schedule, step);
}
JsonlChunkReuseAction::DropRemoved => {
schedule.metadata_only_drop_actions += 1;
schedule.dropped_byte_count =
schedule.dropped_byte_count.saturating_add(step.byte_count);
}
}
}
schedule.max_parallel_candidate_actions = schedule.candidate_output_actions;
schedule
}
fn record_candidate_output(schedule: &mut JsonlWitnessReuseSchedule, step: &JsonlChunkReuseStep) {
schedule.candidate_output_actions += 1;
schedule.candidate_output_line_count = schedule
.candidate_output_line_count
.saturating_add(step.line_count);
schedule.candidate_output_byte_count = schedule
.candidate_output_byte_count
.saturating_add(step.byte_count);
}
fn candidate_actions_are_deterministically_ordered(actions: &[JsonlChunkReuseStep]) -> bool {
let mut expected_candidate_index = 0;
let mut drops_started = false;
for step in actions {
match step.candidate_index {
Some(candidate_index)
if !drops_started && candidate_index == expected_candidate_index =>
{
expected_candidate_index += 1;
}
Some(_) => return false,
None => drops_started = true,
}
}
true
}
fn work_batch_kind(step: &JsonlChunkReuseStep) -> JsonlWitnessWorkBatchKind {
if step.candidate_index.is_some() {
JsonlWitnessWorkBatchKind::CandidateOutput
} else {
JsonlWitnessWorkBatchKind::MetadataOnlyDrop
}
}
fn push_work_batch(
batches: &mut Vec<JsonlWitnessWorkBatch>,
kind: JsonlWitnessWorkBatchKind,
actions: Vec<JsonlChunkReuseStep>,
) {
let index = batches.len();
batches.push(build_work_batch(index, kind, actions));
}
fn build_work_batch(
index: usize,
kind: JsonlWitnessWorkBatchKind,
actions: Vec<JsonlChunkReuseStep>,
) -> JsonlWitnessWorkBatch {
let action_count = actions.len();
let candidate_start_index = actions.iter().filter_map(|step| step.candidate_index).min();
let candidate_end_index = actions
.iter()
.filter_map(|step| step.candidate_index)
.max()
.map(|index| index.saturating_add(1));
let line_count = actions
.iter()
.fold(0_usize, |total, step| total.saturating_add(step.line_count));
let byte_count = actions
.iter()
.fold(0_u64, |total, step| total.saturating_add(step.byte_count));
JsonlWitnessWorkBatch {
index,
kind,
action_count,
candidate_start_index,
candidate_end_index,
line_count,
byte_count,
actions,
}
}
fn work_batches_are_deterministically_ordered(batches: &[JsonlWitnessWorkBatch]) -> bool {
let mut expected_candidate_index = 0;
let mut drops_started = false;
for (expected_batch_index, batch) in batches.iter().enumerate() {
if batch.index != expected_batch_index {
return false;
}
match batch.kind {
JsonlWitnessWorkBatchKind::CandidateOutput if !drops_started => {
if batch.candidate_start_index != Some(expected_candidate_index) {
return false;
}
expected_candidate_index += batch.action_count;
if batch.candidate_end_index != Some(expected_candidate_index) {
return false;
}
}
JsonlWitnessWorkBatchKind::CandidateOutput => return false,
JsonlWitnessWorkBatchKind::MetadataOnlyDrop => drops_started = true,
}
}
true
}
fn sum_chunk_bytes(chunks: &[JsonlChunkWitness]) -> u64 {
chunks.iter().map(|chunk| chunk.byte_count).sum()
}
struct PendingChunk {
index: usize,
start_line: usize,
line_count: usize,
byte_count: u64,
bytes: Vec<u8>,
line_lengths: Vec<usize>,
}
impl PendingChunk {
fn new(index: usize, start_line: usize, chunk_size_lines: usize) -> Self {
Self {
index,
start_line,
line_count: 0,
byte_count: 0,
bytes: Vec::new(),
line_lengths: Vec::with_capacity(chunk_size_lines),
}
}
fn is_full(&self, chunk_size_lines: usize) -> bool {
self.line_count == chunk_size_lines
}
fn push_line(&mut self, line: &[u8], bytes_read: usize) -> io::Result<()> {
self.line_count = self
.line_count
.checked_add(1)
.ok_or_else(|| io_invalid_data("chunk line count overflowed usize"))?;
self.byte_count = checked_add_bytes(self.byte_count, bytes_read, "chunk byte count")?;
self.line_lengths.push(bytes_read);
self.bytes.extend_from_slice(line);
Ok(())
}
fn finish(self) -> io::Result<JsonlChunkWitness> {
let mut hasher = Sha256::new();
hasher.update(CHUNK_DOMAIN);
hasher.update(index_to_bytes(self.index)?);
hasher.update(index_to_bytes(self.start_line)?);
let mut first_line_hash = None;
let mut last_line_hash = None;
let mut offset = 0usize;
for (line_index, bytes_read) in self.line_lengths.iter().copied().enumerate() {
let next_offset = offset
.checked_add(bytes_read)
.ok_or_else(|| io_invalid_data("chunk byte offset overflowed usize"))?;
let line = self
.bytes
.get(offset..next_offset)
.ok_or_else(|| io_invalid_data("chunk line range exceeded buffered bytes"))?;
let line_hash = sha256_hex(line);
if first_line_hash.is_none() {
first_line_hash = Some(line_hash.clone());
}
last_line_hash = Some(line_hash);
hasher.update(index_to_bytes(line_index)?);
hasher.update(length_to_bytes(bytes_read)?);
hasher.update(line);
offset = next_offset;
}
Ok(JsonlChunkWitness {
index: self.index,
start_line: self.start_line,
line_count: self.line_count,
byte_count: self.byte_count,
hash: hex_encode(&hasher.finalize()),
first_line_hash,
last_line_hash,
})
}
}
fn build_parallel_chunks<R: BufRead>(
mut reader: R,
chunk_size_lines: usize,
max_parallelism: usize,
) -> io::Result<(Vec<JsonlChunkWitness>, usize, u64)> {
thread::scope(|scope| {
let (result_sender, result_receiver) =
mpsc::channel::<(usize, io::Result<JsonlChunkWitness>)>();
let mut task_senders = Vec::with_capacity(max_parallelism);
let mut handles = Vec::with_capacity(max_parallelism);
for _ in 0..max_parallelism {
let (task_sender, task_receiver) = mpsc::sync_channel::<PendingChunk>(1);
let result_sender = result_sender.clone();
task_senders.push(task_sender);
handles.push(scope.spawn(move || {
while let Ok(chunk) = task_receiver.recv() {
let index = chunk.index;
if result_sender.send((index, chunk.finish())).is_err() {
break;
}
}
}));
}
drop(result_sender);
let mut line_count = 0usize;
let mut byte_count = 0u64;
let mut chunk_count = 0usize;
let mut next_worker = 0usize;
let mut current_chunk = PendingChunk::new(0, 0, chunk_size_lines);
let mut line = Vec::new();
loop {
line.clear();
let bytes_read = reader.read_until(b'\n', &mut line)?;
if bytes_read == 0 {
break;
}
if current_chunk.is_full(chunk_size_lines) {
send_pending_chunk(&task_senders, &mut next_worker, current_chunk)?;
chunk_count = chunk_count
.checked_add(1)
.ok_or_else(|| io_invalid_data("JSONL chunk count overflowed usize"))?;
current_chunk = PendingChunk::new(chunk_count, line_count, chunk_size_lines);
}
let next_line_count = line_count
.checked_add(1)
.ok_or_else(|| io_invalid_data("JSONL line count overflowed usize"))?;
let next_byte_count = checked_add_bytes(byte_count, bytes_read, "JSONL byte count")?;
current_chunk.push_line(&line, bytes_read)?;
line_count = next_line_count;
byte_count = next_byte_count;
}
if current_chunk.line_count > 0 {
send_pending_chunk(&task_senders, &mut next_worker, current_chunk)?;
chunk_count = chunk_count
.checked_add(1)
.ok_or_else(|| io_invalid_data("JSONL chunk count overflowed usize"))?;
}
drop(task_senders);
let chunks = collect_parallel_chunks(&result_receiver, chunk_count)?;
for handle in handles {
handle
.join()
.map_err(|_| io::Error::other("parallel JSONL witness worker panicked"))?;
}
Ok((chunks, line_count, byte_count))
})
}
fn send_pending_chunk(
task_senders: &[mpsc::SyncSender<PendingChunk>],
next_worker: &mut usize,
chunk: PendingChunk,
) -> io::Result<()> {
let sender = task_senders
.get(*next_worker)
.ok_or_else(|| io_invalid_data("parallel JSONL witness worker set was empty"))?;
*next_worker = (*next_worker + 1) % task_senders.len();
sender.send(chunk).map_err(|_| {
io::Error::new(
io::ErrorKind::BrokenPipe,
"parallel JSONL witness worker disconnected",
)
})
}
fn collect_parallel_chunks(
result_receiver: &mpsc::Receiver<(usize, io::Result<JsonlChunkWitness>)>,
chunk_count: usize,
) -> io::Result<Vec<JsonlChunkWitness>> {
let mut chunks_by_index = vec![None; chunk_count];
for _ in 0..chunk_count {
let (index, chunk) = result_receiver.recv().map_err(|_| {
io::Error::new(
io::ErrorKind::BrokenPipe,
"parallel JSONL witness worker result channel closed",
)
})?;
let slot = chunks_by_index
.get_mut(index)
.ok_or_else(|| io_invalid_data("parallel JSONL witness chunk index out of range"))?;
if slot.replace(chunk?).is_some() {
return Err(io_invalid_data(
"parallel JSONL witness duplicate chunk index",
));
}
}
chunks_by_index
.into_iter()
.enumerate()
.map(|(index, chunk)| {
chunk.ok_or_else(|| {
io_invalid_data(format!(
"parallel JSONL witness missing chunk index {index}"
))
})
})
.collect()
}
struct ChunkBuilder {
index: usize,
start_line: usize,
line_count: usize,
byte_count: u64,
hasher: Sha256,
first_line_hash: Option<String>,
last_line_hash: Option<String>,
}
impl ChunkBuilder {
fn new(index: usize, start_line: usize) -> io::Result<Self> {
let mut hasher = Sha256::new();
hasher.update(CHUNK_DOMAIN);
hasher.update(index_to_bytes(index)?);
hasher.update(index_to_bytes(start_line)?);
Ok(Self {
index,
start_line,
line_count: 0,
byte_count: 0,
hasher,
first_line_hash: None,
last_line_hash: None,
})
}
fn push_line(&mut self, line: &[u8], bytes_read: usize) -> io::Result<()> {
let line_hash = sha256_hex(line);
if self.first_line_hash.is_none() {
self.first_line_hash = Some(line_hash.clone());
}
self.last_line_hash = Some(line_hash);
self.hasher.update(index_to_bytes(self.line_count)?);
self.hasher.update(length_to_bytes(bytes_read)?);
self.hasher.update(line);
self.line_count = self
.line_count
.checked_add(1)
.ok_or_else(|| io_invalid_data("chunk line count overflowed usize"))?;
self.byte_count = checked_add_bytes(self.byte_count, bytes_read, "chunk byte count")?;
Ok(())
}
fn finish(self) -> JsonlChunkWitness {
let hash = hex_encode(&self.hasher.finalize());
JsonlChunkWitness {
index: self.index,
start_line: self.start_line,
line_count: self.line_count,
byte_count: self.byte_count,
hash,
first_line_hash: self.first_line_hash,
last_line_hash: self.last_line_hash,
}
}
}
fn compute_root_hash(
chunk_size_lines: usize,
line_count: usize,
byte_count: u64,
chunks: &[JsonlChunkWitness],
) -> io::Result<String> {
let mut hasher = Sha256::new();
hasher.update(ROOT_DOMAIN);
hash_field(&mut hasher, JSONL_WITNESS_SCHEMA_VERSION.as_bytes())?;
hasher.update(index_to_bytes(chunk_size_lines)?);
hasher.update(index_to_bytes(line_count)?);
hasher.update(byte_count.to_le_bytes());
hasher.update(index_to_bytes(chunks.len())?);
for chunk in chunks {
hasher.update(index_to_bytes(chunk.index)?);
hasher.update(index_to_bytes(chunk.start_line)?);
hasher.update(index_to_bytes(chunk.line_count)?);
hasher.update(chunk.byte_count.to_le_bytes());
hash_field(&mut hasher, chunk.hash.as_bytes())?;
hash_optional_field(&mut hasher, chunk.first_line_hash.as_deref())?;
hash_optional_field(&mut hasher, chunk.last_line_hash.as_deref())?;
}
Ok(hex_encode(&hasher.finalize()))
}
fn hash_field(hasher: &mut Sha256, bytes: &[u8]) -> io::Result<()> {
hasher.update(length_to_bytes(bytes.len())?);
hasher.update(FIELD_SEPARATOR);
hasher.update(bytes);
hasher.update(FIELD_SEPARATOR);
Ok(())
}
fn hash_optional_field(hasher: &mut Sha256, value: Option<&str>) -> io::Result<()> {
match value {
Some(value) => {
hasher.update([1]);
hash_field(hasher, value.as_bytes())?;
}
None => hasher.update([0]),
}
Ok(())
}
fn checked_add_bytes(total: u64, delta: usize, label: &'static str) -> io::Result<u64> {
let delta = length_to_u64(delta)?;
checked_add_u64(total, delta, label)
}
fn checked_add_u64(total: u64, delta: u64, label: &'static str) -> io::Result<u64> {
total
.checked_add(delta)
.ok_or_else(|| io_invalid_data(format!("{label} overflowed u64")))
}
fn index_to_bytes(value: usize) -> io::Result<[u8; 8]> {
length_to_bytes(value)
}
fn length_to_bytes(value: usize) -> io::Result<[u8; 8]> {
Ok(length_to_u64(value)?.to_le_bytes())
}
fn length_to_u64(value: usize) -> io::Result<u64> {
u64::try_from(value).map_err(|_| io_invalid_data("length exceeded u64"))
}
fn sha256_hex(bytes: &[u8]) -> String {
let mut hasher = Sha256::new();
hasher.update(bytes);
hex_encode(&hasher.finalize())
}
fn io_invalid_data(message: impl Into<String>) -> io::Error {
io::Error::new(io::ErrorKind::InvalidData, message.into())
}
#[cfg(test)]
mod tests {
use super::*;
use std::io::Cursor;
fn witness(input: &[u8], chunk_size_lines: usize) -> JsonlMerkleWitness {
build_jsonl_merkle_witness(Cursor::new(input), chunk_size_lines).unwrap()
}
fn materialize_plan(
base_input: &[u8],
candidate_input: &[u8],
base: &JsonlMerkleWitness,
candidate: &JsonlMerkleWitness,
plan: &JsonlWitnessReusePlan,
) -> io::Result<(JsonlWitnessReuseMaterialization, Vec<u8>)> {
let mut base_reader = Cursor::new(base_input);
let mut candidate_reader = Cursor::new(candidate_input);
let mut output = Vec::new();
let summary = materialize_jsonl_witness_reuse_plan(
&mut base_reader,
&mut candidate_reader,
&mut output,
base,
candidate,
plan,
)?;
Ok((summary, output))
}
#[test]
fn builds_deterministic_chunk_witnesses() {
let input = b"{\"id\":\"a\"}\n{\"id\":\"b\"}\n{\"id\":\"c\"}\n";
let first = build_jsonl_merkle_witness(Cursor::new(input), 2).unwrap();
let second = build_jsonl_merkle_witness(Cursor::new(input), 2).unwrap();
assert_eq!(first, second);
assert_eq!(first.schema_version, JSONL_WITNESS_SCHEMA_VERSION);
assert_eq!(first.chunk_size_lines, 2);
assert_eq!(first.line_count, 3);
assert_eq!(first.byte_count, u64::try_from(input.len()).unwrap());
assert_eq!(first.chunks.len(), 2);
assert_eq!(first.chunks[0].index, 0);
assert_eq!(first.chunks[0].start_line, 0);
assert_eq!(first.chunks[0].line_count, 2);
assert_eq!(first.chunks[1].index, 1);
assert_eq!(first.chunks[1].start_line, 2);
assert_eq!(first.chunks[1].line_count, 1);
assert_eq!(
first.chunks[0].first_line_hash.as_deref(),
Some(sha256_hex(b"{\"id\":\"a\"}\n").as_str())
);
assert_eq!(
first.chunks[1].last_line_hash.as_deref(),
Some(sha256_hex(b"{\"id\":\"c\"}\n").as_str())
);
}
#[test]
fn parallel_witness_matches_serial_witness() {
let input =
b"{\"id\":\"a\"}\n{\"id\":\"b\"}\n{\"id\":\"c\"}\n{\"id\":\"d\"}\n{\"id\":\"e\"}";
let serial = build_jsonl_merkle_witness(Cursor::new(input), 2).unwrap();
let parallel = build_jsonl_merkle_witness_parallel(Cursor::new(input), 2, 3).unwrap();
assert_eq!(parallel, serial);
}
#[test]
fn parallel_witness_parallelism_one_uses_serial_output() {
let input = b"a\nb\nc\n";
let serial = build_jsonl_merkle_witness(Cursor::new(input), 1).unwrap();
let parallel = build_jsonl_merkle_witness_parallel(Cursor::new(input), 1, 1).unwrap();
assert_eq!(parallel, serial);
}
#[test]
fn parallel_witness_rejects_zero_parallelism() {
let err = build_jsonl_merkle_witness_parallel(Cursor::new(b"a\n"), 1, 0).unwrap_err();
assert_eq!(err.kind(), io::ErrorKind::InvalidInput);
}
#[test]
fn root_changes_when_a_byte_changes() {
let original = build_jsonl_merkle_witness(Cursor::new(b"{\"id\":\"a\"}\n"), 2).unwrap();
let changed = build_jsonl_merkle_witness(Cursor::new(b"{\"id\":\"b\"}\n"), 2).unwrap();
assert_ne!(original.root_hash, changed.root_hash);
assert_ne!(original.chunks[0].hash, changed.chunks[0].hash);
}
#[test]
fn root_changes_when_line_order_changes() {
let first = build_jsonl_merkle_witness(Cursor::new(b"a\nb\n"), 2).unwrap();
let second = build_jsonl_merkle_witness(Cursor::new(b"b\na\n"), 2).unwrap();
assert_ne!(first.root_hash, second.root_hash);
}
#[test]
fn final_line_without_newline_is_counted_and_hashed() {
let witness = build_jsonl_merkle_witness(Cursor::new(b"a\nb"), 1).unwrap();
assert_eq!(witness.line_count, 2);
assert_eq!(witness.byte_count, 3);
assert_eq!(witness.chunks.len(), 2);
assert_eq!(
witness.chunks[1].first_line_hash.as_deref(),
Some(sha256_hex(b"b").as_str())
);
assert_eq!(
witness.chunks[1].last_line_hash.as_deref(),
Some(sha256_hex(b"b").as_str())
);
}
#[test]
fn zero_chunk_size_is_rejected() {
let err = build_jsonl_merkle_witness(Cursor::new(b"a\n"), 0).unwrap_err();
assert_eq!(err.kind(), io::ErrorKind::InvalidInput);
}
#[test]
fn empty_input_has_stable_empty_root() {
let first = build_jsonl_merkle_witness(Cursor::new(Vec::new()), 2).unwrap();
let second = build_jsonl_merkle_witness(Cursor::new(Vec::new()), 2).unwrap();
assert_eq!(first, second);
assert_eq!(first.line_count, 0);
assert_eq!(first.byte_count, 0);
assert!(first.chunks.is_empty());
assert!(!first.root_hash.is_empty());
}
#[test]
fn comparison_reports_identical_witnesses_as_reusable() {
let base = witness(b"a\nb\nc\n", 2);
let candidate = witness(b"a\nb\nc\n", 2);
let comparison = compare_jsonl_merkle_witnesses(&base, &candidate);
assert!(comparison.schema_versions_match);
assert!(comparison.chunk_size_lines_match);
assert!(comparison.root_hashes_match);
assert!(!comparison.drift_detected);
assert_eq!(comparison.comparable_chunk_count, 2);
assert_eq!(comparison.unchanged_chunks, 2);
assert_eq!(comparison.changed_chunks, 0);
assert_eq!(comparison.added_chunks, 0);
assert_eq!(comparison.removed_chunks, 0);
assert_eq!(comparison.unchanged_byte_count, candidate.byte_count);
assert_eq!(comparison.safe_reuse_prefix_chunks, 2);
assert_eq!(comparison.first_changed_chunk_index, None);
}
#[test]
fn comparison_localizes_changed_chunk() {
let base = witness(b"a\nb\nc\n", 1);
let candidate = witness(b"a\nB\nc\n", 1);
let comparison = compare_jsonl_merkle_witnesses(&base, &candidate);
assert!(comparison.drift_detected);
assert!(!comparison.root_hashes_match);
assert_eq!(comparison.comparable_chunk_count, 3);
assert_eq!(comparison.unchanged_chunks, 2);
assert_eq!(comparison.changed_chunks, 1);
assert_eq!(comparison.added_chunks, 0);
assert_eq!(comparison.removed_chunks, 0);
assert_eq!(comparison.safe_reuse_prefix_chunks, 1);
assert_eq!(comparison.first_changed_chunk_index, Some(1));
assert_eq!(
comparison.changed_base_byte_count,
base.chunks[1].byte_count
);
assert_eq!(
comparison.changed_candidate_byte_count,
candidate.chunks[1].byte_count
);
}
#[test]
fn comparison_reports_tail_appends_without_marking_prefix_changed() {
let base = witness(b"a\nb\n", 1);
let candidate = witness(b"a\nb\nc\n", 1);
let comparison = compare_jsonl_merkle_witnesses(&base, &candidate);
assert!(comparison.drift_detected);
assert_eq!(comparison.comparable_chunk_count, 2);
assert_eq!(comparison.unchanged_chunks, 2);
assert_eq!(comparison.changed_chunks, 0);
assert_eq!(comparison.added_chunks, 1);
assert_eq!(comparison.removed_chunks, 0);
assert_eq!(comparison.added_byte_count, candidate.chunks[2].byte_count);
assert_eq!(comparison.removed_byte_count, 0);
assert_eq!(comparison.safe_reuse_prefix_chunks, 2);
assert_eq!(comparison.first_changed_chunk_index, Some(2));
}
#[test]
fn comparison_rejects_incompatible_chunk_sizes_for_reuse() {
let base = witness(b"a\nb\nc\n", 1);
let candidate = witness(b"a\nb\nc\n", 2);
let comparison = compare_jsonl_merkle_witnesses(&base, &candidate);
assert!(comparison.drift_detected);
assert!(!comparison.chunk_size_lines_match);
assert_eq!(comparison.comparable_chunk_count, 0);
assert_eq!(comparison.unchanged_chunks, 0);
assert_eq!(comparison.changed_chunks, 0);
assert_eq!(comparison.added_chunks, candidate.chunks.len());
assert_eq!(comparison.removed_chunks, base.chunks.len());
assert_eq!(comparison.safe_reuse_prefix_chunks, 0);
assert_eq!(comparison.first_changed_chunk_index, Some(0));
}
#[test]
fn reuse_plan_reuses_identical_candidate_chunks_in_order() {
let base = witness(b"a\nb\nc\n", 1);
let candidate = witness(b"a\nb\nc\n", 1);
let plan = plan_jsonl_witness_reuse(&base, &candidate);
let actions: Vec<_> = plan.actions.iter().map(|step| step.action).collect();
assert_eq!(
actions,
vec![
JsonlChunkReuseAction::ReuseUnchanged,
JsonlChunkReuseAction::ReuseUnchanged,
JsonlChunkReuseAction::ReuseUnchanged,
]
);
assert!(
plan.actions
.iter()
.enumerate()
.all(|(index, step)| step.candidate_index == Some(index))
);
assert_eq!(plan.comparison.safe_reuse_prefix_chunks, 3);
}
#[test]
fn reuse_plan_separates_changed_and_added_candidate_chunks() {
let base = witness(b"a\nb\n", 1);
let candidate = witness(b"a\nB\nc\n", 1);
let plan = plan_jsonl_witness_reuse(&base, &candidate);
let actions: Vec<_> = plan.actions.iter().map(|step| step.action).collect();
assert_eq!(
actions,
vec![
JsonlChunkReuseAction::ReuseUnchanged,
JsonlChunkReuseAction::RebuildCandidate,
JsonlChunkReuseAction::ReadAdded,
]
);
assert_eq!(plan.actions[1].base_index, Some(1));
assert_eq!(plan.actions[1].candidate_index, Some(1));
assert_eq!(plan.actions[2].base_index, None);
assert_eq!(plan.actions[2].candidate_index, Some(2));
assert_eq!(plan.schedule.total_actions, 3);
assert_eq!(plan.schedule.candidate_output_actions, 3);
assert_eq!(plan.schedule.metadata_only_drop_actions, 0);
assert_eq!(plan.schedule.reusable_actions, 1);
assert_eq!(plan.schedule.rebuild_actions, 1);
assert_eq!(plan.schedule.read_added_actions, 1);
assert_eq!(plan.schedule.max_parallel_candidate_actions, 3);
assert!(plan.schedule.deterministic_candidate_order);
assert_eq!(
plan.schedule.candidate_output_line_count,
candidate.line_count
);
assert_eq!(
plan.schedule.candidate_output_byte_count,
candidate.byte_count
);
assert_eq!(
plan.schedule.reusable_byte_count,
candidate.chunks[0].byte_count
);
assert_eq!(
plan.schedule.rebuild_byte_count,
candidate.chunks[1].byte_count
);
assert_eq!(
plan.schedule.read_added_byte_count,
candidate.chunks[2].byte_count
);
}
#[test]
fn reuse_plan_appends_removed_base_chunks_after_candidate_actions() {
let base = witness(b"a\nb\nc\n", 1);
let candidate = witness(b"a\n", 1);
let plan = plan_jsonl_witness_reuse(&base, &candidate);
let actions: Vec<_> = plan.actions.iter().map(|step| step.action).collect();
assert_eq!(
actions,
vec![
JsonlChunkReuseAction::ReuseUnchanged,
JsonlChunkReuseAction::DropRemoved,
JsonlChunkReuseAction::DropRemoved,
]
);
assert_eq!(plan.actions[0].candidate_index, Some(0));
assert_eq!(plan.actions[1].base_index, Some(1));
assert_eq!(plan.actions[1].candidate_index, None);
assert_eq!(plan.actions[2].base_index, Some(2));
assert_eq!(plan.schedule.candidate_output_actions, 1);
assert_eq!(plan.schedule.metadata_only_drop_actions, 2);
assert_eq!(plan.schedule.max_parallel_candidate_actions, 1);
assert_eq!(
plan.schedule.dropped_byte_count,
base.chunks[1].byte_count + base.chunks[2].byte_count
);
assert!(plan.schedule.deterministic_candidate_order);
}
#[test]
fn reuse_plan_rebuilds_all_candidate_chunks_when_chunk_sizes_differ() {
let base = witness(b"a\nb\nc\n", 1);
let candidate = witness(b"a\nb\nc\n", 2);
let plan = plan_jsonl_witness_reuse(&base, &candidate);
let actions: Vec<_> = plan.actions.iter().map(|step| step.action).collect();
assert_eq!(
actions,
vec![
JsonlChunkReuseAction::RebuildCandidate,
JsonlChunkReuseAction::RebuildCandidate,
JsonlChunkReuseAction::DropRemoved,
JsonlChunkReuseAction::DropRemoved,
JsonlChunkReuseAction::DropRemoved,
]
);
assert!(plan.actions[0].base_index.is_none());
assert_eq!(plan.actions[0].candidate_index, Some(0));
assert_eq!(plan.actions[2].base_index, Some(0));
assert_eq!(plan.comparison.comparable_chunk_count, 0);
}
#[test]
fn reuse_materialization_reconstructs_candidate_bytes_from_mixed_actions() {
let base_input: &[u8] = b"a\nb\nc\n";
let candidate_input: &[u8] = b"a\nB\nc\nd\n";
let base = witness(base_input, 1);
let candidate = witness(candidate_input, 1);
let plan = plan_jsonl_witness_reuse(&base, &candidate);
let (summary, output) =
materialize_plan(base_input, candidate_input, &base, &candidate, &plan).unwrap();
assert_eq!(output, candidate_input);
assert_eq!(summary.reused_chunks, 2);
assert_eq!(summary.rebuilt_chunks, 1);
assert_eq!(summary.read_added_chunks, 1);
assert_eq!(summary.dropped_chunks, 0);
assert_eq!(summary.output_byte_count, candidate.byte_count);
assert_eq!(
summary.reused_byte_count,
candidate.chunks[0].byte_count + candidate.chunks[2].byte_count
);
assert_eq!(summary.rebuilt_byte_count, candidate.chunks[1].byte_count);
assert_eq!(
summary.read_added_byte_count,
candidate.chunks[3].byte_count
);
}
#[test]
fn reuse_materialization_counts_removed_base_chunks_without_emitting_them() {
let base_input: &[u8] = b"a\nb\nc\n";
let candidate_input: &[u8] = b"a\n";
let base = witness(base_input, 1);
let candidate = witness(candidate_input, 1);
let plan = plan_jsonl_witness_reuse(&base, &candidate);
let (summary, output) =
materialize_plan(base_input, candidate_input, &base, &candidate, &plan).unwrap();
assert_eq!(output, candidate_input);
assert_eq!(summary.reused_chunks, 1);
assert_eq!(summary.dropped_chunks, 2);
assert_eq!(summary.output_byte_count, candidate.byte_count);
assert_eq!(
summary.dropped_byte_count,
base.chunks[1].byte_count + base.chunks[2].byte_count
);
}
#[test]
fn reuse_materialization_rebuilds_candidate_when_chunk_sizes_differ() {
let base_input: &[u8] = b"a\nb\nc\n";
let candidate_input: &[u8] = b"a\nb\nc\n";
let base = witness(base_input, 1);
let candidate = witness(candidate_input, 2);
let plan = plan_jsonl_witness_reuse(&base, &candidate);
let (summary, output) =
materialize_plan(base_input, candidate_input, &base, &candidate, &plan).unwrap();
assert_eq!(output, candidate_input);
assert_eq!(summary.reused_chunks, 0);
assert_eq!(summary.rebuilt_chunks, candidate.chunks.len());
assert_eq!(summary.dropped_chunks, base.chunks.len());
assert_eq!(summary.output_byte_count, candidate.byte_count);
}
#[test]
fn reuse_materialization_rejects_tampered_reuse_action() {
let base_input: &[u8] = b"a\n";
let candidate_input: &[u8] = b"A\n";
let base = witness(base_input, 1);
let candidate = witness(candidate_input, 1);
let mut plan = plan_jsonl_witness_reuse(&base, &candidate);
for step in &mut plan.actions {
step.action = JsonlChunkReuseAction::ReuseUnchanged;
}
plan.schedule = build_reuse_schedule(&plan.actions);
let err =
materialize_plan(base_input, candidate_input, &base, &candidate, &plan).unwrap_err();
assert_eq!(err.kind(), io::ErrorKind::InvalidData);
assert!(err.to_string().contains("no longer matches"));
}
#[test]
fn parallel_work_plan_batches_candidate_actions_before_metadata_drops() {
let base = witness(b"a\nb\nc\nd\n", 1);
let candidate = witness(b"a\nB\nc\n", 1);
let reuse_plan = plan_jsonl_witness_reuse(&base, &candidate);
let work_plan = plan_jsonl_witness_parallel_work(&reuse_plan, 2).unwrap();
assert_eq!(work_plan.max_parallelism, 2);
assert_eq!(work_plan.total_batches, 3);
assert_eq!(work_plan.candidate_output_batches, 2);
assert_eq!(work_plan.metadata_only_drop_batches, 1);
assert!(work_plan.deterministic_batch_order);
assert_eq!(
work_plan.batches[0].kind,
JsonlWitnessWorkBatchKind::CandidateOutput
);
assert_eq!(work_plan.batches[0].action_count, 2);
assert_eq!(work_plan.batches[0].candidate_start_index, Some(0));
assert_eq!(work_plan.batches[0].candidate_end_index, Some(2));
assert_eq!(
work_plan.batches[0]
.actions
.iter()
.map(|step| step.action)
.collect::<Vec<_>>(),
vec![
JsonlChunkReuseAction::ReuseUnchanged,
JsonlChunkReuseAction::RebuildCandidate,
]
);
assert_eq!(
work_plan.batches[1].kind,
JsonlWitnessWorkBatchKind::CandidateOutput
);
assert_eq!(work_plan.batches[1].action_count, 1);
assert_eq!(work_plan.batches[1].candidate_start_index, Some(2));
assert_eq!(work_plan.batches[1].candidate_end_index, Some(3));
assert_eq!(
work_plan.batches[1].line_count,
candidate.chunks[2].line_count
);
assert_eq!(
work_plan.batches[1].byte_count,
candidate.chunks[2].byte_count
);
assert_eq!(
work_plan.batches[2].kind,
JsonlWitnessWorkBatchKind::MetadataOnlyDrop
);
assert_eq!(work_plan.batches[2].action_count, 1);
assert_eq!(work_plan.batches[2].candidate_start_index, None);
assert_eq!(work_plan.batches[2].candidate_end_index, None);
assert_eq!(work_plan.batches[2].line_count, base.chunks[3].line_count);
assert_eq!(work_plan.batches[2].byte_count, base.chunks[3].byte_count);
}
#[test]
fn parallel_work_plan_rejects_zero_parallelism() {
let base = witness(b"a\n", 1);
let candidate = witness(b"a\n", 1);
let reuse_plan = plan_jsonl_witness_reuse(&base, &candidate);
let err = plan_jsonl_witness_parallel_work(&reuse_plan, 0).unwrap_err();
assert_eq!(err.kind(), io::ErrorKind::InvalidInput);
}
#[test]
fn parallel_work_plan_rebuilds_incompatible_candidate_chunks_in_waves() {
let base = witness(b"a\nb\nc\n", 1);
let candidate = witness(b"a\nb\nc\n", 2);
let reuse_plan = plan_jsonl_witness_reuse(&base, &candidate);
let work_plan = plan_jsonl_witness_parallel_work(&reuse_plan, 1).unwrap();
assert_eq!(work_plan.total_batches, 5);
assert_eq!(work_plan.candidate_output_batches, 2);
assert_eq!(work_plan.metadata_only_drop_batches, 3);
assert!(work_plan.deterministic_batch_order);
assert_eq!(
work_plan
.batches
.iter()
.map(|batch| batch.kind)
.collect::<Vec<_>>(),
vec![
JsonlWitnessWorkBatchKind::CandidateOutput,
JsonlWitnessWorkBatchKind::CandidateOutput,
JsonlWitnessWorkBatchKind::MetadataOnlyDrop,
JsonlWitnessWorkBatchKind::MetadataOnlyDrop,
JsonlWitnessWorkBatchKind::MetadataOnlyDrop,
]
);
assert_eq!(work_plan.batches[0].candidate_start_index, Some(0));
assert_eq!(work_plan.batches[1].candidate_start_index, Some(1));
}
}