use crate::error::{Error, Result};
use crate::parser::repair_clustering::{
resolve_clustering_value_layout, skip_covered_slice, ByteSkip,
};
use crate::storage::sstable::version_gate::VersionGates;
const METADATA_TYPE_STATS: u32 = 2;
const METADATA_TYPE_HEADER: u32 = 3;
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum RepairField<T> {
Decoded(T),
Unparsed,
}
impl<T> RepairField<T> {
pub fn decoded(&self) -> Option<&T> {
match self {
RepairField::Decoded(v) => Some(v),
RepairField::Unparsed => None,
}
}
pub fn is_decoded(&self) -> bool {
matches!(self, RepairField::Decoded(_))
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct RepairMetadata {
pub repaired_at: i64,
pub pending_repair: RepairField<Option<[u8; 16]>>,
pub is_transient: RepairField<bool>,
pub repaired_at_decoded: bool,
}
impl RepairMetadata {
pub fn unrepaired_default() -> Self {
RepairMetadata {
repaired_at: 0,
pending_repair: RepairField::Unparsed,
is_transient: RepairField::Unparsed,
repaired_at_decoded: false,
}
}
}
const METADATA_COMPONENT_CRC_LEN: usize = 4;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
struct StatsComponentBounds {
start: usize,
end: usize,
}
fn stats_component_bounds(input: &[u8]) -> Result<Option<StatsComponentBounds>> {
parse_statistics_toc(input).stats_bounds()
}
#[derive(Debug, Clone)]
pub(crate) struct StatisticsToc {
header_offset: Option<usize>,
stats_bounds: std::result::Result<Option<StatsComponentBounds>, String>,
}
impl StatisticsToc {
pub(crate) fn header_offset(&self) -> Option<usize> {
self.header_offset
}
fn stats_bounds(&self) -> Result<Option<StatsComponentBounds>> {
match &self.stats_bounds {
Ok(v) => Ok(*v),
Err(msg) => Err(Error::Corruption(msg.clone())),
}
}
}
pub(crate) fn parse_statistics_toc(input: &[u8]) -> StatisticsToc {
crate::parser::toc_walk_metrics::record_toc_walk();
let (header_offset, stats_bounds) = walk_statistics_toc(input);
StatisticsToc {
header_offset,
stats_bounds,
}
}
fn walk_statistics_toc(
input: &[u8],
) -> (
Option<usize>,
std::result::Result<Option<StatsComponentBounds>, String>,
) {
if input.len() < 8 {
return (
None,
Err(format!(
"Statistics.db too short for a metadata TOC header: {} bytes",
input.len()
)),
);
}
let num_components = u32::from_be_bytes([input[0], input[1], input[2], input[3]]);
if num_components == 0 || num_components > 100 {
return (
None,
Err(format!(
"Statistics.db TOC component count out of range: {num_components}"
)),
);
}
let toc_start = 8usize;
let entry_size = 8usize;
let toc_size = match (num_components as usize)
.checked_mul(entry_size)
.and_then(|n| n.checked_add(toc_start))
{
Some(n) => n,
None => {
return (
None,
Err(format!(
"Statistics.db TOC size overflow for {num_components} components"
)),
)
}
};
if input.len() < toc_size {
return (
None,
Err(format!(
"Statistics.db TOC truncated: need {toc_size} bytes, have {}",
input.len()
)),
);
}
let mut stats_off: Option<usize> = None;
let mut header_off: Option<usize> = None;
let mut offsets: Vec<usize> = Vec::with_capacity(num_components as usize);
for i in 0..num_components as usize {
let entry = match i
.checked_mul(entry_size)
.and_then(|n| n.checked_add(toc_start))
{
Some(e) => e,
None => {
return (
header_off,
Err(format!(
"Statistics.db TOC entry offset overflow at index {i}"
)),
);
}
};
let ty = u32::from_be_bytes([
input[entry],
input[entry + 1],
input[entry + 2],
input[entry + 3],
]);
let off = u32::from_be_bytes([
input[entry + 4],
input[entry + 5],
input[entry + 6],
input[entry + 7],
]) as usize;
offsets.push(off);
if ty == METADATA_TYPE_STATS {
stats_off = Some(off);
} else if ty == METADATA_TYPE_HEADER && header_off.is_none() {
header_off = Some(off);
}
}
let Some(start) = stats_off else {
return (header_off, Ok(None));
};
let next_boundary = offsets
.iter()
.copied()
.filter(|&o| o > start)
.min()
.unwrap_or(input.len());
let end = next_boundary.saturating_sub(METADATA_COMPONENT_CRC_LEN);
if start >= end || end > input.len() {
return (
header_off,
Err(format!(
"STATS component range invalid: start {start}, derived end {end}, \
Statistics.db {} bytes",
input.len()
)),
);
}
(header_off, Ok(Some(StatsComponentBounds { start, end })))
}
struct Cursor<'a> {
bytes: &'a [u8],
pos: usize,
}
impl<'a> Cursor<'a> {
fn new(bytes: &'a [u8]) -> Self {
Cursor { bytes, pos: 0 }
}
fn need(&self, n: usize) -> Result<usize> {
let end = self
.pos
.checked_add(n)
.ok_or_else(|| Error::Corruption("STATS cursor overflow".to_string()))?;
if end > self.bytes.len() {
return Err(Error::Corruption(format!(
"STATS component truncated: need {n} bytes at offset {} but only {} remain",
self.pos,
self.bytes.len().saturating_sub(self.pos),
)));
}
Ok(end)
}
fn skip(&mut self, n: usize) -> Result<()> {
let end = self.need(n)?;
self.pos = end;
Ok(())
}
fn read_i32(&mut self) -> Result<i32> {
let end = self.need(4)?;
let v = i32::from_be_bytes([
self.bytes[self.pos],
self.bytes[self.pos + 1],
self.bytes[self.pos + 2],
self.bytes[self.pos + 3],
]);
self.pos = end;
Ok(v)
}
fn read_i64(&mut self) -> Result<i64> {
let end = self.need(8)?;
let mut buf = [0u8; 8];
buf.copy_from_slice(&self.bytes[self.pos..end]);
self.pos = end;
Ok(i64::from_be_bytes(buf))
}
fn read_u32(&mut self) -> Result<u32> {
let end = self.need(4)?;
let v = u32::from_be_bytes([
self.bytes[self.pos],
self.bytes[self.pos + 1],
self.bytes[self.pos + 2],
self.bytes[self.pos + 3],
]);
self.pos = end;
Ok(v)
}
fn read_f64(&mut self) -> Result<f64> {
let end = self.need(8)?;
let mut buf = [0u8; 8];
buf.copy_from_slice(&self.bytes[self.pos..end]);
self.pos = end;
Ok(f64::from_be_bytes(buf))
}
fn read_u8(&mut self) -> Result<u8> {
let end = self.need(1)?;
let v = self.bytes[self.pos];
self.pos = end;
Ok(v)
}
fn read_u16(&mut self) -> Result<u16> {
let end = self.need(2)?;
let v = u16::from_be_bytes([self.bytes[self.pos], self.bytes[self.pos + 1]]);
self.pos = end;
Ok(v)
}
fn read_uuid(&mut self) -> Result<[u8; 16]> {
let end = self.need(16)?;
let mut buf = [0u8; 16];
buf.copy_from_slice(&self.bytes[self.pos..end]);
self.pos = end;
Ok(buf)
}
fn read_unsigned_vint(&mut self) -> Result<u64> {
let first = self.read_u8()?;
let extra = first.leading_ones() as usize;
if extra == 0 {
return Ok(first as u64);
}
if extra > 8 {
return Err(Error::Corruption(format!(
"invalid unsigned VInt: {extra} extra bytes"
)));
}
let mut value: u64 = 0;
for _ in 0..extra {
let b = self.read_u8()?;
value = (value << 8) | b as u64;
}
if extra < 8 {
let mask = (1u8 << (8 - extra)) as u64;
let first_bits = first as u64 & (mask - 1);
value |= first_bits << (8 * extra);
}
Ok(value)
}
fn read_bytes(&mut self, n: usize) -> Result<Vec<u8>> {
let end = self.need(n)?;
let v = self.bytes[self.pos..end].to_vec();
self.pos = end;
Ok(v)
}
}
impl ByteSkip for Cursor<'_> {
fn read_u8(&mut self) -> Result<u8> {
Cursor::read_u8(self)
}
fn read_u16(&mut self) -> Result<u16> {
Cursor::read_u16(self)
}
fn read_unsigned_vint(&mut self) -> Result<u64> {
Cursor::read_unsigned_vint(self)
}
fn skip(&mut self, n: usize) -> Result<()> {
Cursor::skip(self, n)
}
}
fn skip_estimated_histogram(c: &mut Cursor) -> Result<()> {
let count = c.read_i32()?;
if count < 0 {
return Err(Error::Corruption(format!(
"negative EstimatedHistogram bucket count {count}"
)));
}
let bytes = (count as usize)
.checked_mul(16)
.ok_or_else(|| Error::Corruption("EstimatedHistogram size overflow".to_string()))?;
c.skip(bytes)
}
fn skip_tombstone_histogram(c: &mut Cursor, modern: bool) -> Result<()> {
let _max_bin_size = c.read_i32()?;
let size = c.read_i32()?;
if size < 0 {
return Err(Error::Corruption(format!(
"negative TombstoneHistogram size {size}"
)));
}
let entry_width = if modern { 12 } else { 16 };
let bytes = (size as usize)
.checked_mul(entry_width)
.ok_or_else(|| Error::Corruption("TombstoneHistogram size overflow".to_string()))?;
c.skip(bytes)
}
fn uses_modern_tombstone_histogram(gates: &VersionGates) -> bool {
match gates {
VersionGates::Big(g) => g.has_uint_deletion_time,
VersionGates::Bti(_) => true,
}
}
#[derive(Debug, Clone, Copy)]
struct RepairWalkGates {
modern_histogram: bool,
has_improved_min_max: bool,
has_legacy_min_max: bool,
has_pending_repair: bool,
has_is_transient: bool,
}
impl RepairWalkGates {
fn from(gates: &VersionGates) -> Self {
match gates {
VersionGates::Big(g) => RepairWalkGates {
modern_histogram: g.has_uint_deletion_time,
has_improved_min_max: g.has_improved_min_max,
has_legacy_min_max: g.has_legacy_min_max,
has_pending_repair: g.has_pending_repair,
has_is_transient: g.has_is_transient,
},
VersionGates::Bti(g) => RepairWalkGates {
modern_histogram: true,
has_improved_min_max: g.has_improved_min_max,
has_legacy_min_max: g.has_legacy_min_max,
has_pending_repair: g.has_pending_repair,
has_is_transient: g.has_is_transient,
},
}
}
}
fn skip_legacy_min_max(c: &mut Cursor) -> Result<()> {
for _ in 0..2 {
let count = c.read_i32()?;
if count < 0 {
return Err(Error::Corruption(format!(
"negative legacy min/max clustering count {count}"
)));
}
for _ in 0..count {
let len = c.read_u16()? as usize;
c.skip(len)?;
}
}
Ok(())
}
fn try_skip_improved_min_max(c: &mut Cursor) -> Result<bool> {
let layouts = {
let count = ByteSkip::read_unsigned_vint(c)? as usize;
let mut v = Vec::with_capacity(count.min(64));
for _ in 0..count {
let len = ByteSkip::read_unsigned_vint(c)? as usize;
let bytes = c.read_bytes(len)?;
let name = std::str::from_utf8(&bytes).map_err(|_| {
Error::Corruption("clusteringTypes entry is not valid UTF-8".to_string())
})?;
v.push(resolve_clustering_value_layout(name));
}
v
};
skip_covered_slice(c, &layouts)
}
fn skip_commit_log_intervals(c: &mut Cursor) -> Result<()> {
let size = c.read_i32()?;
if size < 0 {
return Err(Error::Corruption(format!(
"negative commitLogIntervals size {size}"
)));
}
let bytes = (size as usize)
.checked_mul(24)
.ok_or_else(|| Error::Corruption("commitLogIntervals size overflow".to_string()))?;
c.skip(bytes)
}
pub fn parse_repair_metadata(input: &[u8], gates: Option<&VersionGates>) -> Result<RepairMetadata> {
let walk = gates.map(RepairWalkGates::from);
let Some(bounds) = stats_component_bounds(input)? else {
return Ok(RepairMetadata::unrepaired_default());
};
let modern_histogram = walk.map(|w| w.modern_histogram).unwrap_or(false);
let mut c = Cursor::new(&input[bounds.start..bounds.end]);
skip_estimated_histogram(&mut c)?;
skip_estimated_histogram(&mut c)?;
c.skip(8 + 4)?;
c.skip(8 + 8)?;
c.skip(4 + 4)?;
c.skip(4 + 4)?;
c.skip(8)?;
skip_tombstone_histogram(&mut c, modern_histogram)?;
let _sstable_level = c.read_i32()?;
let repaired_at = c.read_i64()?;
let Some(walk) = walk else {
return Ok(RepairMetadata {
repaired_at,
pending_repair: RepairField::Unparsed,
is_transient: RepairField::Unparsed,
repaired_at_decoded: true,
});
};
if walk.has_improved_min_max {
if !try_skip_improved_min_max(&mut c)? {
return Ok(RepairMetadata {
repaired_at,
pending_repair: RepairField::Unparsed,
is_transient: RepairField::Unparsed,
repaired_at_decoded: true,
});
}
} else if walk.has_legacy_min_max {
skip_legacy_min_max(&mut c)?;
}
c.skip(1)?;
c.skip(8 + 8)?;
c.skip(8 + 4)?; skip_commit_log_intervals(&mut c)?;
let pending_repair = if walk.has_pending_repair {
let present = c.read_u8()?;
if present != 0 {
RepairField::Decoded(Some(c.read_uuid()?))
} else {
RepairField::Decoded(None)
}
} else {
RepairField::Unparsed
};
let is_transient = if walk.has_is_transient {
RepairField::Decoded(c.read_u8()? != 0)
} else {
RepairField::Unparsed
};
Ok(RepairMetadata {
repaired_at,
pending_repair,
is_transient,
repaired_at_decoded: true,
})
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct TableCounts {
pub partition_count: u64,
pub total_rows: Option<u64>,
}
pub fn read_table_counts(input: &[u8], gates: Option<&VersionGates>) -> Result<TableCounts> {
read_table_counts_from_bounds(input, stats_component_bounds(input)?, gates)
}
pub(crate) fn read_table_counts_with_toc(
input: &[u8],
toc: &StatisticsToc,
gates: Option<&VersionGates>,
) -> Result<TableCounts> {
read_table_counts_from_bounds(input, toc.stats_bounds()?, gates)
}
fn read_table_counts_from_bounds(
input: &[u8],
bounds: Option<StatsComponentBounds>,
gates: Option<&VersionGates>,
) -> Result<TableCounts> {
let walk = gates.map(RepairWalkGates::from);
let Some(bounds) = bounds else {
return Ok(TableCounts {
partition_count: 0,
total_rows: None,
});
};
let modern_histogram = walk.map(|w| w.modern_histogram).unwrap_or(false);
let mut c = Cursor::new(&input[bounds.start..bounds.end]);
let partition_count = sum_estimated_histogram_counts(&mut c)?;
skip_estimated_histogram(&mut c)?;
let Some(walk) = walk else {
return Ok(TableCounts {
partition_count,
total_rows: None,
});
};
c.skip(8 + 4)?; c.skip(8 + 8)?; c.skip(4 + 4)?; c.skip(4 + 4)?; c.skip(8)?; skip_tombstone_histogram(&mut c, modern_histogram)?;
c.skip(4 + 8)?;
if walk.has_improved_min_max {
if !try_skip_improved_min_max(&mut c)? {
return Ok(TableCounts {
partition_count,
total_rows: None,
});
}
} else if walk.has_legacy_min_max {
skip_legacy_min_max(&mut c)?;
}
c.skip(1)?;
c.skip(8)?; let total_rows = c.read_i64()?;
let total_rows = u64::try_from(total_rows).ok();
Ok(TableCounts {
partition_count,
total_rows,
})
}
fn sum_estimated_histogram_counts(c: &mut Cursor) -> Result<u64> {
let count = c.read_i32()?;
if count < 0 {
return Err(Error::Corruption(format!(
"negative EstimatedHistogram bucket count {count}"
)));
}
let mut sum: u64 = 0;
for _ in 0..count {
let _offset = c.read_i64()?;
let bucket = c.read_i64()?;
if bucket < 0 {
return Err(Error::Corruption(format!(
"negative EstimatedHistogram bucket value {bucket}"
)));
}
sum = sum.saturating_add(bucket as u64);
}
Ok(sum)
}
const NO_DELETION_TIME: i64 = i64::MAX;
#[derive(Debug, Clone)]
pub struct StatsExtras {
pub max_local_deletion_time: i64,
pub tombstone_drop_times: Vec<(i64, u64)>,
pub max_timestamp: Option<i64>,
pub max_ttl: Option<i64>,
}
impl Default for StatsExtras {
fn default() -> Self {
StatsExtras {
max_local_deletion_time: NO_DELETION_TIME,
tombstone_drop_times: Vec::new(),
max_timestamp: None,
max_ttl: None,
}
}
}
pub fn parse_stats_extras(input: &[u8], gates: Option<&VersionGates>) -> Result<StatsExtras> {
parse_stats_extras_from_bounds(input, stats_component_bounds(input)?, gates)
}
pub(crate) fn parse_stats_extras_with_toc(
input: &[u8],
toc: &StatisticsToc,
gates: Option<&VersionGates>,
) -> Result<StatsExtras> {
parse_stats_extras_from_bounds(input, toc.stats_bounds()?, gates)
}
fn parse_stats_extras_from_bounds(
input: &[u8],
bounds: Option<StatsComponentBounds>,
gates: Option<&VersionGates>,
) -> Result<StatsExtras> {
let Some(bounds) = bounds else {
return Ok(StatsExtras::default());
};
let modern = gates.map(uses_modern_tombstone_histogram).unwrap_or(false);
let mut c = Cursor::new(&input[bounds.start..bounds.end]);
skip_estimated_histogram(&mut c)?;
skip_estimated_histogram(&mut c)?;
c.skip(8 + 4)?;
c.skip(8)?; let raw_max_timestamp = c.read_i64()?;
let max_timestamp = decode_max_timestamp(raw_max_timestamp);
let _min_ldt = c.read_u32()?;
let raw_max_ldt = c.read_u32()?;
let max_local_deletion_time = decode_max_local_deletion_time(raw_max_ldt, modern);
c.skip(4)?; let raw_max_ttl = c.read_i32()?;
let max_ttl = if raw_max_ttl > 0 {
Some(i64::from(raw_max_ttl))
} else {
None
};
c.read_f64()?;
let tombstone_drop_times = read_tombstone_histogram(&mut c, modern)?;
Ok(StatsExtras {
max_local_deletion_time,
tombstone_drop_times,
max_timestamp,
max_ttl,
})
}
pub(crate) const NO_MAX_TIMESTAMP_SENTINEL: i64 = i64::MIN;
pub(crate) fn decode_max_timestamp(raw: i64) -> Option<i64> {
if raw == NO_MAX_TIMESTAMP_SENTINEL {
None
} else {
Some(raw)
}
}
fn decode_max_local_deletion_time(raw: u32, modern: bool) -> i64 {
if modern {
if raw == u32::MAX {
NO_DELETION_TIME
} else {
raw as i64
}
} else {
let signed = raw as i32;
if signed == i32::MAX {
NO_DELETION_TIME
} else {
signed as i64
}
}
}
fn read_tombstone_histogram(c: &mut Cursor, modern: bool) -> Result<Vec<(i64, u64)>> {
let _max_bin_size = c.read_i32()?;
let size = c.read_i32()?;
if size < 0 {
return Err(Error::Corruption(format!(
"negative TombstoneHistogram size {size}"
)));
}
let entry_width = if modern { 12usize } else { 16usize };
let required = (size as usize)
.checked_mul(entry_width)
.ok_or_else(|| Error::Corruption("TombstoneHistogram size overflow".to_string()))?;
c.need(required)?;
let mut out = Vec::with_capacity(size as usize);
for _ in 0..size {
if modern {
let point = c.read_i64()?;
let value = c.read_i32()?;
out.push((point, value as u64));
} else {
let point = c.read_f64()?;
let value = c.read_i64()?;
out.push((point as i64, value as u64));
}
}
Ok(out)
}
#[cfg(test)]
mod tests {
use super::*;
use crate::storage::sstable::version_gate::BigVersionGates;
fn synthetic_statistics(modern_histogram: bool, repaired_at: i64) -> Vec<u8> {
let mut stats = Vec::new();
let est_hist = |b: &mut Vec<u8>| {
b.extend_from_slice(&0i32.to_be_bytes()); };
est_hist(&mut stats); est_hist(&mut stats); stats.extend_from_slice(&(-1i64).to_be_bytes()); stats.extend_from_slice(&0i32.to_be_bytes()); stats.extend_from_slice(&100i64.to_be_bytes()); stats.extend_from_slice(&200i64.to_be_bytes()); stats.extend_from_slice(&i32::MAX.to_be_bytes()); stats.extend_from_slice(&i32::MAX.to_be_bytes()); stats.extend_from_slice(&0i32.to_be_bytes()); stats.extend_from_slice(&0i32.to_be_bytes()); stats.extend_from_slice(&(-1.0f64).to_be_bytes()); stats.extend_from_slice(&0i32.to_be_bytes());
stats.extend_from_slice(&0i32.to_be_bytes());
let _ = modern_histogram; stats.extend_from_slice(&0i32.to_be_bytes()); stats.extend_from_slice(&repaired_at.to_be_bytes());
let toc_len = 4 + 4 + 4 * 8; let comp0_off = toc_len; let comp1_off = comp0_off + 1;
let comp3_off = comp1_off + 1;
let stats_off = comp3_off + 1;
let mut out = Vec::new();
out.extend_from_slice(&4u32.to_be_bytes()); out.extend_from_slice(&0u32.to_be_bytes()); for (ty, off) in [
(0u32, comp0_off as u32),
(1u32, comp1_off as u32),
(2u32, stats_off as u32), (3u32, comp3_off as u32),
] {
out.extend_from_slice(&ty.to_be_bytes());
out.extend_from_slice(&off.to_be_bytes());
}
out.extend_from_slice(&[0u8, 0u8, 0u8]);
debug_assert_eq!(out.len(), stats_off);
out.extend_from_slice(&stats);
out.extend_from_slice(&0u32.to_be_bytes()); out
}
fn synthetic_statistics_extras(modern: bool, raw_max_ldt: u32, bins: &[(i64, u64)]) -> Vec<u8> {
synthetic_statistics_extras_ts(modern, raw_max_ldt, bins, 100, 200, 0)
}
fn synthetic_statistics_extras_ts(
modern: bool,
raw_max_ldt: u32,
bins: &[(i64, u64)],
min_timestamp: i64,
max_timestamp: i64,
max_ttl: i32,
) -> Vec<u8> {
let mut stats = Vec::new();
stats.extend_from_slice(&0i32.to_be_bytes());
stats.extend_from_slice(&0i32.to_be_bytes());
stats.extend_from_slice(&(-1i64).to_be_bytes());
stats.extend_from_slice(&0i32.to_be_bytes());
stats.extend_from_slice(&min_timestamp.to_be_bytes());
stats.extend_from_slice(&max_timestamp.to_be_bytes());
stats.extend_from_slice(&0u32.to_be_bytes());
stats.extend_from_slice(&raw_max_ldt.to_be_bytes());
stats.extend_from_slice(&0i32.to_be_bytes());
stats.extend_from_slice(&max_ttl.to_be_bytes());
stats.extend_from_slice(&(-1.0f64).to_be_bytes());
stats.extend_from_slice(&100i32.to_be_bytes()); stats.extend_from_slice(&(bins.len() as i32).to_be_bytes());
for &(point, value) in bins {
if modern {
stats.extend_from_slice(&point.to_be_bytes()); stats.extend_from_slice(&(value as i32).to_be_bytes()); } else {
stats.extend_from_slice(&(point as f64).to_be_bytes()); stats.extend_from_slice(&(value as i64).to_be_bytes()); }
}
stats.extend_from_slice(&0i32.to_be_bytes());
stats.extend_from_slice(&0i64.to_be_bytes());
let toc_len = 4 + 4 + 4 * 8;
let comp0_off = toc_len;
let comp1_off = comp0_off + 1;
let comp3_off = comp1_off + 1;
let stats_off = comp3_off + 1;
let mut out = Vec::new();
out.extend_from_slice(&4u32.to_be_bytes());
out.extend_from_slice(&0u32.to_be_bytes());
for (ty, off) in [
(0u32, comp0_off as u32),
(1u32, comp1_off as u32),
(2u32, stats_off as u32),
(3u32, comp3_off as u32),
] {
out.extend_from_slice(&ty.to_be_bytes());
out.extend_from_slice(&off.to_be_bytes());
}
out.extend_from_slice(&[0u8, 0u8, 0u8]);
debug_assert_eq!(out.len(), stats_off);
out.extend_from_slice(&stats);
out.extend_from_slice(&0u32.to_be_bytes()); out
}
fn nb_gates() -> VersionGates {
VersionGates::Big(BigVersionGates::from_version("nb").expect("nb gates"))
}
fn oa_gates() -> VersionGates {
VersionGates::Big(BigVersionGates::from_version("oa").expect("oa gates"))
}
fn da_gates() -> VersionGates {
use crate::storage::sstable::version_gate::BtiVersionGates;
VersionGates::Bti(BtiVersionGates::from_version("da").expect("da gates"))
}
fn push_uvint(buf: &mut Vec<u8>, v: u64) {
assert!(
v < 0x80,
"test uvint helper only handles single-byte values"
);
buf.push(v as u8);
}
fn synthetic_full_nb(
repaired_at: i64,
pending_repair: Option<[u8; 16]>,
is_transient: bool,
) -> Vec<u8> {
let mut stats = Vec::new();
stats.extend_from_slice(&0i32.to_be_bytes()); stats.extend_from_slice(&0i32.to_be_bytes()); stats.extend_from_slice(&(-1i64).to_be_bytes()); stats.extend_from_slice(&0i32.to_be_bytes()); stats.extend_from_slice(&100i64.to_be_bytes()); stats.extend_from_slice(&200i64.to_be_bytes()); stats.extend_from_slice(&i32::MAX.to_be_bytes()); stats.extend_from_slice(&i32::MAX.to_be_bytes()); stats.extend_from_slice(&0i32.to_be_bytes()); stats.extend_from_slice(&0i32.to_be_bytes()); stats.extend_from_slice(&(-1.0f64).to_be_bytes()); stats.extend_from_slice(&0i32.to_be_bytes()); stats.extend_from_slice(&0i32.to_be_bytes()); stats.extend_from_slice(&0i32.to_be_bytes()); stats.extend_from_slice(&repaired_at.to_be_bytes()); stats.extend_from_slice(&0i32.to_be_bytes());
stats.extend_from_slice(&0i32.to_be_bytes());
stats.push(0x00); stats.extend_from_slice(&0u64.to_be_bytes()); stats.extend_from_slice(&0u64.to_be_bytes()); stats.extend_from_slice(&(-1i64).to_be_bytes()); stats.extend_from_slice(&0i32.to_be_bytes()); stats.extend_from_slice(&0i32.to_be_bytes()); match pending_repair {
Some(uuid) => {
stats.push(0x01);
stats.extend_from_slice(&uuid);
}
None => stats.push(0x00),
}
stats.push(if is_transient { 0x01 } else { 0x00 }); wrap_stats_last(stats)
}
fn synthetic_full_da(
repaired_at: i64,
pending_repair: Option<[u8; 16]>,
is_transient: bool,
) -> Vec<u8> {
let mut stats = Vec::new();
stats.extend_from_slice(&0i32.to_be_bytes()); stats.extend_from_slice(&0i32.to_be_bytes()); stats.extend_from_slice(&(-1i64).to_be_bytes()); stats.extend_from_slice(&0i32.to_be_bytes()); stats.extend_from_slice(&100i64.to_be_bytes()); stats.extend_from_slice(&200i64.to_be_bytes()); stats.extend_from_slice(&u32::MAX.to_be_bytes()); stats.extend_from_slice(&u32::MAX.to_be_bytes()); stats.extend_from_slice(&0i32.to_be_bytes()); stats.extend_from_slice(&0i32.to_be_bytes()); stats.extend_from_slice(&(-1.0f64).to_be_bytes()); stats.extend_from_slice(&0i32.to_be_bytes()); stats.extend_from_slice(&0i32.to_be_bytes()); stats.extend_from_slice(&0i32.to_be_bytes()); stats.extend_from_slice(&repaired_at.to_be_bytes()); push_uvint(&mut stats, 1); let ty = b"org.apache.cassandra.db.marshal.Int32Type";
push_uvint(&mut stats, ty.len() as u64);
stats.extend_from_slice(ty);
stats.push(1);
stats.extend_from_slice(&0u16.to_be_bytes());
stats.push(6);
stats.extend_from_slice(&0u16.to_be_bytes());
stats.push(0x00); stats.extend_from_slice(&0u64.to_be_bytes()); stats.extend_from_slice(&0u64.to_be_bytes()); stats.extend_from_slice(&(-1i64).to_be_bytes()); stats.extend_from_slice(&0i32.to_be_bytes()); stats.extend_from_slice(&0i32.to_be_bytes()); match pending_repair {
Some(uuid) => {
stats.push(0x01);
stats.extend_from_slice(&uuid);
}
None => stats.push(0x00),
}
stats.push(if is_transient { 0x01 } else { 0x00 }); wrap_stats_last(stats)
}
fn wrap_stats_last(stats: Vec<u8>) -> Vec<u8> {
let toc_len = 4 + 4 + 2 * 8;
let comp0_off = toc_len;
let stats_off = comp0_off + 1;
let mut out = Vec::new();
out.extend_from_slice(&2u32.to_be_bytes());
out.extend_from_slice(&0u32.to_be_bytes());
for (ty, off) in [(0u32, comp0_off as u32), (2u32, stats_off as u32)] {
out.extend_from_slice(&ty.to_be_bytes());
out.extend_from_slice(&off.to_be_bytes());
}
out.push(0u8); debug_assert_eq!(out.len(), stats_off);
out.extend_from_slice(&stats);
out.extend_from_slice(&0u32.to_be_bytes()); out
}
#[test]
fn full_walk_nb_decodes_null_pending_and_transient() {
let bytes = synthetic_full_nb(0, None, false);
let md = parse_repair_metadata(&bytes, Some(&nb_gates())).expect("decode");
assert_eq!(md.repaired_at, 0);
assert!(md.repaired_at_decoded);
assert_eq!(md.pending_repair, RepairField::Decoded(None));
assert_eq!(md.is_transient, RepairField::Decoded(false));
}
#[test]
fn full_walk_nb_decodes_pending_uuid_and_transient_true() {
let uuid = [
0x11, 0x22, 0x33, 0x44, 0x55, 0x66, 0x77, 0x88, 0x99, 0xAA, 0xBB, 0xCC, 0xDD, 0xEE,
0xFF, 0x00,
];
let bytes = synthetic_full_nb(1_700_000_000_000, Some(uuid), true);
let md = parse_repair_metadata(&bytes, Some(&nb_gates())).expect("decode");
assert_eq!(md.repaired_at, 1_700_000_000_000);
assert_eq!(md.pending_repair, RepairField::Decoded(Some(uuid)));
assert_eq!(md.is_transient, RepairField::Decoded(true));
}
#[test]
fn full_walk_da_decodes_pending_uuid_and_transient() {
let uuid = [0xABu8; 16];
let bytes = synthetic_full_da(42, Some(uuid), true);
let md = parse_repair_metadata(&bytes, Some(&da_gates())).expect("decode");
assert_eq!(md.repaired_at, 42);
assert_eq!(md.pending_repair, RepairField::Decoded(Some(uuid)));
assert_eq!(md.is_transient, RepairField::Decoded(true));
}
#[test]
fn full_walk_da_decodes_null_pending_non_transient() {
let bytes = synthetic_full_da(0, None, false);
let md = parse_repair_metadata(&bytes, Some(&da_gates())).expect("decode");
assert_eq!(md.pending_repair, RepairField::Decoded(None));
assert_eq!(md.is_transient, RepairField::Decoded(false));
}
#[test]
fn full_walk_none_gates_reports_unparsed() {
let bytes = synthetic_full_nb(7, Some([0x01u8; 16]), true);
let md = parse_repair_metadata(&bytes, None).expect("decode repairedAt only");
assert_eq!(md.repaired_at, 7);
assert_eq!(md.pending_repair, RepairField::Unparsed);
assert_eq!(md.is_transient, RepairField::Unparsed);
}
#[test]
fn full_walk_truncated_pending_fails_closed() {
let mut bytes = synthetic_full_nb(0, Some([0x07u8; 16]), true);
bytes.truncate(bytes.len() - (4 + 1 + 8));
assert!(
parse_repair_metadata(&bytes, Some(&nb_gates())).is_err(),
"a truncated pendingRepair UUID must fail closed, not default"
);
}
#[test]
fn read_unsigned_vint_matches_encode_vuint() {
use crate::parser::vint::encode_vuint;
for v in [0u64, 1, 63, 127, 128, 255, 300, 16383, 16384, 1_000_000] {
let enc = encode_vuint(v);
let mut c = Cursor::new(&enc);
assert_eq!(c.read_unsigned_vint().expect("decode vint"), v, "v={v}");
}
}
#[test]
fn stats_extras_real_max_ldt_nb() {
let bytes = synthetic_statistics_extras(false, 1_700_000_000, &[]);
let extras = parse_stats_extras(&bytes, Some(&nb_gates())).expect("decode");
assert_eq!(extras.max_local_deletion_time, 1_700_000_000);
assert!(extras.tombstone_drop_times.is_empty());
assert_eq!(extras.max_timestamp, Some(200));
assert_eq!(extras.max_ttl, None);
}
#[test]
fn stats_extras_decodes_authoritative_max_ttl() {
let bytes =
synthetic_statistics_extras_ts(false, i32::MAX as u32, &[], 100, 200, 10_000_000);
let extras = parse_stats_extras(&bytes, Some(&nb_gates())).expect("decode");
assert_eq!(
extras.max_ttl,
Some(10_000_000),
"authoritative maxTTL must be decoded from STATS field 9"
);
let none_bytes = synthetic_statistics_extras_ts(false, i32::MAX as u32, &[], 100, 200, 0);
let none_extras = parse_stats_extras(&none_bytes, Some(&nb_gates())).expect("decode");
assert_eq!(
none_extras.max_ttl, None,
"maxTTL of 0 (no expiring cells) must surface as None"
);
}
#[test]
fn stats_extras_decodes_authoritative_max_timestamp_not_min() {
let min_ts = 1_000_000i64;
let max_ts = 5_000_000i64;
let bytes = synthetic_statistics_extras_ts(false, i32::MAX as u32, &[], min_ts, max_ts, 0);
let extras = parse_stats_extras(&bytes, Some(&nb_gates())).expect("decode");
assert_eq!(
extras.max_timestamp,
Some(max_ts),
"parsed max_timestamp must be the true max, not the min"
);
assert_ne!(
extras.max_timestamp,
Some(min_ts),
"max_timestamp must NOT equal the min placeholder"
);
}
#[test]
fn stats_extras_max_timestamp_long_min_sentinel_is_none() {
let bytes =
synthetic_statistics_extras_ts(false, i32::MAX as u32, &[], i64::MAX, i64::MIN, 0);
let extras = parse_stats_extras(&bytes, Some(&nb_gates())).expect("decode");
assert_eq!(extras.max_timestamp, None);
}
#[test]
fn stats_extras_default_max_timestamp_is_none() {
let extras = StatsExtras::default();
assert_eq!(extras.max_timestamp, None);
}
#[test]
fn stats_extras_nb_sentinel_maps_to_i64_max() {
let bytes = synthetic_statistics_extras(false, i32::MAX as u32, &[]);
let extras = parse_stats_extras(&bytes, Some(&nb_gates())).expect("decode");
assert_eq!(extras.max_local_deletion_time, i64::MAX);
let extras_none = parse_stats_extras(&bytes, None).expect("decode");
assert_eq!(extras_none.max_local_deletion_time, i64::MAX);
}
#[test]
fn stats_extras_modern_sentinel_maps_to_i64_max() {
let bytes = synthetic_statistics_extras(true, u32::MAX, &[]);
let extras = parse_stats_extras(&bytes, Some(&oa_gates())).expect("decode");
assert_eq!(extras.max_local_deletion_time, i64::MAX);
}
#[test]
fn stats_extras_modern_real_max_ldt() {
let raw: u32 = 3_000_000_000; let bytes = synthetic_statistics_extras(true, raw, &[]);
let extras = parse_stats_extras(&bytes, Some(&oa_gates())).expect("decode");
assert_eq!(extras.max_local_deletion_time, raw as i64);
}
#[test]
fn stats_extras_histogram_nb_width() {
let bins = [(1_700_000_000i64, 3u64), (1_700_000_100i64, 7u64)];
let bytes = synthetic_statistics_extras(false, i32::MAX as u32, &bins);
let extras = parse_stats_extras(&bytes, Some(&nb_gates())).expect("decode");
assert_eq!(
extras.tombstone_drop_times,
vec![(1_700_000_000, 3), (1_700_000_100, 7)]
);
}
#[test]
fn stats_extras_histogram_modern_width() {
let bins = [(1_700_000_000i64, 11u64), (1_700_000_500i64, 2u64)];
let bytes = synthetic_statistics_extras(true, u32::MAX, &bins);
let extras = parse_stats_extras(&bytes, Some(&oa_gates())).expect("decode");
assert_eq!(
extras.tombstone_drop_times,
vec![(1_700_000_000, 11), (1_700_000_500, 2)]
);
}
#[test]
fn stats_extras_empty_histogram() {
let bytes = synthetic_statistics_extras(false, i32::MAX as u32, &[]);
let extras = parse_stats_extras(&bytes, Some(&nb_gates())).expect("decode");
assert!(extras.tombstone_drop_times.is_empty());
}
#[test]
fn stats_extras_missing_component_returns_default() {
let mut out = Vec::new();
out.extend_from_slice(&1u32.to_be_bytes()); out.extend_from_slice(&0u32.to_be_bytes()); out.extend_from_slice(&3u32.to_be_bytes()); out.extend_from_slice(&16u32.to_be_bytes()); let extras = parse_stats_extras(&out, None).expect("default");
assert_eq!(extras.max_local_deletion_time, i64::MAX);
assert!(extras.tombstone_drop_times.is_empty());
}
#[test]
fn stats_extras_truncated_fails_closed() {
let mut bytes =
synthetic_statistics_extras(false, 1_700_000_000, &[(1i64, 1u64), (2i64, 2u64)]);
bytes.truncate(bytes.len() - (4 + 4 + 8 + 16 + 4));
assert!(
parse_stats_extras(&bytes, Some(&nb_gates())).is_err(),
"truncated STATS body must fail closed"
);
}
#[test]
fn tombstone_histogram_huge_size_fails_closed_without_allocating() {
for modern in [false, true] {
let mut buf = Vec::new();
buf.extend_from_slice(&100i32.to_be_bytes()); buf.extend_from_slice(&i32::MAX.to_be_bytes()); let mut c = Cursor::new(&buf);
assert!(
read_tombstone_histogram(&mut c, modern).is_err(),
"huge histogram size (modern={modern}) must fail closed, not allocate"
);
}
}
#[test]
fn decodes_repaired_at_legacy_histogram() {
let bytes = synthetic_statistics(false, 0);
let md = parse_repair_metadata(&bytes, None).expect("decode");
assert_eq!(md.repaired_at, 0);
assert!(md.repaired_at_decoded);
assert_eq!(md.pending_repair, RepairField::Unparsed);
assert_eq!(md.is_transient, RepairField::Unparsed);
assert!(!md.pending_repair.is_decoded());
assert!(!md.is_transient.is_decoded());
}
#[test]
fn decodes_nonzero_repaired_at() {
let bytes = synthetic_statistics(false, 1_700_000_000_000);
let md = parse_repair_metadata(&bytes, None).expect("decode");
assert_eq!(md.repaired_at, 1_700_000_000_000);
assert!(md.repaired_at_decoded);
}
#[test]
fn malformed_toc_fails_closed() {
assert!(parse_repair_metadata(&[0u8, 0, 0], None).is_err());
let mut zero = Vec::new();
zero.extend_from_slice(&0u32.to_be_bytes());
zero.extend_from_slice(&0u32.to_be_bytes());
assert!(parse_repair_metadata(&zero, None).is_err());
let mut huge = Vec::new();
huge.extend_from_slice(&101u32.to_be_bytes());
huge.extend_from_slice(&0u32.to_be_bytes());
assert!(parse_repair_metadata(&huge, None).is_err());
let mut truncated = Vec::new();
truncated.extend_from_slice(&4u32.to_be_bytes()); truncated.extend_from_slice(&0u32.to_be_bytes()); truncated.extend_from_slice(&2u32.to_be_bytes()); assert!(parse_repair_metadata(&truncated, None).is_err());
}
#[test]
fn missing_stats_component_reports_unrepaired_default() {
let mut out = Vec::new();
out.extend_from_slice(&1u32.to_be_bytes()); out.extend_from_slice(&0u32.to_be_bytes()); out.extend_from_slice(&3u32.to_be_bytes()); out.extend_from_slice(&16u32.to_be_bytes()); let md = parse_repair_metadata(&out, None).expect("default");
assert_eq!(md, RepairMetadata::unrepaired_default());
assert!(!md.repaired_at_decoded);
}
#[test]
fn truncated_stats_fails_closed() {
let mut bytes = synthetic_statistics(false, 0);
bytes.truncate(bytes.len() - 4);
let err = parse_repair_metadata(&bytes, None);
assert!(
err.is_err(),
"truncated STATS component must fail closed, got {err:?}"
);
}
#[test]
fn stats_overrunning_component_bound_fails_closed() {
let mut stats = Vec::new();
stats.extend_from_slice(&0i32.to_be_bytes());
let bad_count_pos = stats.len();
stats.extend_from_slice(&0i32.to_be_bytes());
let toc_len = 4 + 4 + 3 * 8;
let stats_off = toc_len;
let next_off = stats_off + stats.len(); let header_bytes = [0xAAu8; 64];
let bad_count: i32 = 8; stats[bad_count_pos..bad_count_pos + 4].copy_from_slice(&bad_count.to_be_bytes());
let mut out = Vec::new();
out.extend_from_slice(&3u32.to_be_bytes());
out.extend_from_slice(&0u32.to_be_bytes());
for (ty, off) in [
(3u32, next_off as u32), (2u32, stats_off as u32),
(0u32, (next_off + header_bytes.len()) as u32),
] {
out.extend_from_slice(&ty.to_be_bytes());
out.extend_from_slice(&off.to_be_bytes());
}
debug_assert_eq!(out.len(), stats_off);
out.extend_from_slice(&stats);
out.extend_from_slice(&header_bytes); out.extend_from_slice(&0u32.to_be_bytes());
let err = parse_repair_metadata(&out, None);
assert!(
err.is_err(),
"a STATS body that overruns its component bound must fail closed \
(not spill into the next component / CRC), got {err:?}"
);
}
#[test]
fn component_bounds_derive_end_from_next_offset() {
let bytes = synthetic_statistics(false, 0);
let bounds = stats_component_bounds(&bytes)
.expect("no error")
.expect("bounds present");
assert_eq!(
bounds.end,
bytes.len() - METADATA_COMPONENT_CRC_LEN,
"STATS-last end must exclude the trailing CRC"
);
assert!(bounds.start < bounds.end);
}
#[test]
fn nonlast_stats_end_excludes_component_crc() {
let mut stats = Vec::new();
stats.extend_from_slice(&0i32.to_be_bytes()); stats.extend_from_slice(&0i32.to_be_bytes()); stats.extend_from_slice(&7i64.to_be_bytes());
let toc_len = 4 + 4 + 2 * 8;
let stats_off = toc_len;
let header_off = stats_off + stats.len() + METADATA_COMPONENT_CRC_LEN;
let header_body = [0xABu8; 8];
let mut out = Vec::new();
out.extend_from_slice(&2u32.to_be_bytes()); out.extend_from_slice(&0u32.to_be_bytes()); for (ty, off) in [(2u32, stats_off as u32), (3u32, header_off as u32)] {
out.extend_from_slice(&ty.to_be_bytes());
out.extend_from_slice(&off.to_be_bytes());
}
debug_assert_eq!(out.len(), stats_off);
out.extend_from_slice(&stats);
let stats_crc = crc32_ieee(&out[stats_off..stats_off + stats.len()]);
out.extend_from_slice(&stats_crc.to_be_bytes()); debug_assert_eq!(out.len(), header_off);
out.extend_from_slice(&header_body);
out.extend_from_slice(&0u32.to_be_bytes());
let bounds = stats_component_bounds(&out)
.expect("no error")
.expect("bounds present");
assert_eq!(
bounds.end,
header_off - METADATA_COMPONENT_CRC_LEN,
"non-last STATS end must exclude the inter-component CRC"
);
assert_eq!(
bounds.end,
stats_off + stats.len(),
"non-last STATS end must equal the true STATS body end"
);
}
fn crc32_ieee(data: &[u8]) -> u32 {
let mut crc: u32 = 0xFFFF_FFFF;
for &byte in data {
crc ^= byte as u32;
for _ in 0..8 {
let mask = (crc & 1).wrapping_neg();
crc = (crc >> 1) ^ (0xEDB8_8320 & mask);
}
}
!crc
}
fn synthetic_counts_nb(partition_buckets: &[(i64, i64)], total_rows: i64) -> Vec<u8> {
let mut stats = Vec::new();
stats.extend_from_slice(&(partition_buckets.len() as i32).to_be_bytes());
for (off, cnt) in partition_buckets {
stats.extend_from_slice(&off.to_be_bytes());
stats.extend_from_slice(&cnt.to_be_bytes());
}
stats.extend_from_slice(&0i32.to_be_bytes()); stats.extend_from_slice(&(-1i64).to_be_bytes()); stats.extend_from_slice(&0i32.to_be_bytes()); stats.extend_from_slice(&100i64.to_be_bytes()); stats.extend_from_slice(&200i64.to_be_bytes()); stats.extend_from_slice(&i32::MAX.to_be_bytes()); stats.extend_from_slice(&i32::MAX.to_be_bytes()); stats.extend_from_slice(&0i32.to_be_bytes()); stats.extend_from_slice(&0i32.to_be_bytes()); stats.extend_from_slice(&(-1.0f64).to_be_bytes()); stats.extend_from_slice(&0i32.to_be_bytes()); stats.extend_from_slice(&0i32.to_be_bytes()); stats.extend_from_slice(&0i32.to_be_bytes()); stats.extend_from_slice(&0i64.to_be_bytes()); stats.extend_from_slice(&0i32.to_be_bytes()); stats.extend_from_slice(&0i32.to_be_bytes()); stats.push(0x00); stats.extend_from_slice(&7u64.to_be_bytes()); stats.extend_from_slice(&total_rows.to_be_bytes()); stats.extend_from_slice(&(-1i64).to_be_bytes()); stats.extend_from_slice(&0i32.to_be_bytes()); stats.extend_from_slice(&0i32.to_be_bytes()); stats.push(0x00); stats.push(0x00); wrap_stats_last(stats)
}
#[test]
fn read_table_counts_sums_partition_histogram_and_total_rows() {
let buf = synthetic_counts_nb(&[(0, 3), (16, 2), (64, 5)], 2000);
let counts = read_table_counts(&buf, Some(&nb_gates())).expect("decode");
assert_eq!(counts.partition_count, 10, "Σ histogram bucket counts");
assert_eq!(counts.total_rows, Some(2000), "totalRows from field 12");
}
#[test]
fn read_table_counts_partition_count_without_gates_total_rows_none() {
let buf = synthetic_counts_nb(&[(0, 4), (8, 6)], 999);
let counts = read_table_counts(&buf, None).expect("decode");
assert_eq!(counts.partition_count, 10);
assert_eq!(
counts.total_rows, None,
"no gates → totalRows honestly absent, never guessed"
);
}
#[test]
fn invalid_stats_range_preserves_resolved_header_offset() {
let toc_len = 4 + 4 + 2 * 8; let header_off = toc_len; let stats_off_bad = 10_000usize;
let mut out = Vec::new();
out.extend_from_slice(&2u32.to_be_bytes()); out.extend_from_slice(&0u32.to_be_bytes()); for (ty, off) in [
(METADATA_TYPE_HEADER, header_off as u32),
(METADATA_TYPE_STATS, stats_off_bad as u32),
] {
out.extend_from_slice(&ty.to_be_bytes());
out.extend_from_slice(&off.to_be_bytes());
}
debug_assert_eq!(out.len(), header_off);
out.extend_from_slice(&[0xABu8; 8]); out.extend_from_slice(&0u32.to_be_bytes());
let toc = parse_statistics_toc(&out);
assert_eq!(
toc.header_offset(),
Some(header_off),
"a corrupt STATS range must NOT nuke the already-resolved HEADER offset \
(issue #2148): EncodingStats must still decode from the HEADER, not the \
marker fallback"
);
assert!(
toc.stats_bounds().is_err(),
"the invalid STATS range must STILL fail closed on its own channel"
);
}
#[test]
fn read_table_counts_no_stats_component_is_all_zero() {
let mut out = Vec::new();
out.extend_from_slice(&1u32.to_be_bytes()); out.extend_from_slice(&0u32.to_be_bytes()); out.extend_from_slice(&0u32.to_be_bytes()); out.extend_from_slice(&16u32.to_be_bytes()); out.push(0u8); out.extend_from_slice(&0u32.to_be_bytes()); let counts = read_table_counts(&out, Some(&nb_gates())).expect("decode");
assert_eq!(counts.partition_count, 0);
assert_eq!(counts.total_rows, None);
}
}