use std::ops::RangeInclusive;
use thiserror::Error;
use zerocopy::little_endian::U64;
use zerocopy::{FromBytes, Immutable, IntoBytes, KnownLayout};
use crate::common::bitpacking::{BitWriter, make_bitmask, packed_bits};
const TAIL_SIZE: usize = size_of::<u64>() - 1;
const DELTA_BITS_RANGE: RangeInclusive<u8> = 1..=(u64::BITS - u8::BITS) as u8;
const MAX_CHUNK_LEN_LOG2: u8 = 7;
pub fn compress(values: &[u64]) -> (Vec<u8>, Parameters) {
let parameters = Parameters::find_best(values);
let compressed = compress_with_parameters(values, parameters);
(compressed, parameters)
}
fn compress_with_parameters(values: &[u64], parameters: Parameters) -> Vec<u8> {
let expected_size = parameters.total_chunks_size_bytes().unwrap() + TAIL_SIZE;
let mut compressed = Vec::with_capacity(expected_size);
for chunk in values.chunks(1 << parameters.chunk_len_log2) {
let first = chunk[0];
let mut w = BitWriter::new(&mut compressed);
w.write(first, parameters.base_bits);
for &value in chunk.iter().skip(1) {
w.write(value - first, parameters.delta_bits);
}
for _ in 0..(1 << parameters.chunk_len_log2) - chunk.len() {
w.write(
make_bitmask::<u64>(parameters.delta_bits),
parameters.delta_bits,
);
}
w.finish(); }
compressed.extend_from_slice(&[0xFF; TAIL_SIZE]);
assert_eq!(compressed.len(), expected_size);
compressed
}
#[derive(Clone, Debug)]
pub struct Reader<'a> {
base_bits: u8,
base_mask: u64,
delta_bits: u8,
delta_mask: u64,
chunk_len_log2: u8,
chunk_len_mask: usize,
chunk_size_bytes: usize,
compressed: &'a [u8],
len: usize,
}
#[derive(Error, Debug)]
#[error("decompression error: {0}")]
pub struct DecompressionError(String);
impl<'a> Reader<'a> {
pub fn new(
parameters: Parameters,
bytes: &'a [u8],
) -> Result<(Self, &'a [u8]), DecompressionError> {
if !parameters.valid() {
return Err(DecompressionError("invalid parameters".to_string()));
}
let total_size_bytes = parameters
.total_chunks_size_bytes()
.and_then(|size| size.checked_add(TAIL_SIZE))
.ok_or_else(|| DecompressionError("invalid parameters".to_string()))?;
let (compressed, bytes) = bytes.split_at_checked(total_size_bytes).ok_or_else(|| {
DecompressionError(format!(
"insufficient length (compressed data, expected {total_size_bytes} bytes, got {})",
bytes.len(),
))
})?;
let result = Self {
base_bits: parameters.base_bits,
base_mask: make_bitmask(parameters.base_bits),
delta_bits: parameters.delta_bits,
delta_mask: make_bitmask(parameters.delta_bits),
chunk_len_log2: parameters.chunk_len_log2,
chunk_len_mask: make_bitmask(parameters.chunk_len_log2),
chunk_size_bytes: parameters.chunk_size_bytes().unwrap(),
compressed,
len: parameters.length.get() as usize,
};
if let Some(max_index) = result.len.checked_sub(1) {
let chunk_offset = (max_index >> result.chunk_len_log2) * result.chunk_size_bytes;
assert!(chunk_offset + size_of::<u64>() <= result.compressed.len());
let max_value_index = result.chunk_len_mask;
if max_value_index > 0 {
let delta_offset_bits =
result.base_bits as usize + (max_value_index - 1) * result.delta_bits as usize;
assert!(
chunk_offset + delta_offset_bits / u8::BITS as usize + size_of::<u64>()
<= result.compressed.len()
);
}
}
Ok((result, bytes))
}
#[cfg(feature = "testing")]
pub fn parameters(&self) -> Parameters {
Parameters {
length: U64::new(self.len as u64),
base_bits: self.base_bits,
delta_bits: self.delta_bits,
chunk_len_log2: self.chunk_len_log2,
}
}
#[inline]
#[expect(clippy::len_without_is_empty, reason = "len() is cheap")]
pub fn len(&self) -> usize {
self.len
}
#[inline]
pub fn get(&self, index: usize) -> Option<u64> {
if index >= self.len {
return None;
}
let chunk_offset = (index >> self.chunk_len_log2) * self.chunk_size_bytes;
let value_index = index & self.chunk_len_mask;
let chunk_ptr = self.compressed.as_ptr().wrapping_add(chunk_offset);
let base = unsafe { read_u64_le(chunk_ptr) } & self.base_mask;
if value_index == 0 {
return Some(base);
}
let delta_offset_bits =
self.base_bits as usize + (value_index - 1) * self.delta_bits as usize;
let delta = (unsafe { read_u64_le(chunk_ptr.add(delta_offset_bits / u8::BITS as usize)) }
>> (delta_offset_bits % u8::BITS as usize))
& self.delta_mask;
Some(base + delta)
}
}
#[inline(always)]
unsafe fn read_u64_le(ptr: *const u8) -> u64 {
unsafe { u64::from_le(ptr.cast::<u64>().read_unaligned()) }
}
#[derive(Clone, Copy, Debug, FromBytes, Immutable, IntoBytes, KnownLayout)]
#[repr(C)]
pub struct Parameters {
pub length: U64,
pub base_bits: u8,
pub delta_bits: u8,
pub chunk_len_log2: u8,
}
impl Parameters {
fn valid(self) -> bool {
u32::from(self.base_bits) <= u64::BITS
&& DELTA_BITS_RANGE.contains(&self.delta_bits)
&& self.chunk_len_log2 <= MAX_CHUNK_LEN_LOG2
}
#[deny(clippy::arithmetic_side_effects, reason = "extra cautious for safety")]
fn chunk_size_bytes(self) -> Option<usize> {
let bits = (self.base_bits as usize).checked_add(
(self.delta_bits as usize).checked_mul(make_bitmask::<usize>(self.chunk_len_log2))?,
)?;
Some(bits.div_ceil(u8::BITS as usize))
}
#[deny(clippy::arithmetic_side_effects, reason = "extra cautious for safety")]
fn total_chunks_size_bytes(self) -> Option<usize> {
let chunks_count = (self.length.get() as usize).div_ceil(1 << self.chunk_len_log2);
chunks_count.checked_mul(self.chunk_size_bytes()?)
}
fn find_best(values: &[u64]) -> Self {
Self::try_all(values)
.min_by_key(|parameters| parameters.total_chunks_size_bytes())
.unwrap()
}
fn try_all(values: &[u64]) -> impl Iterator<Item = Parameters> + use<'_> {
let last_value = values.last().copied().unwrap_or(0);
(0..=MAX_CHUNK_LEN_LOG2)
.map(move |chunk_len_log2| {
let mut delta_bits = *DELTA_BITS_RANGE.start();
for chunk in values.chunks(1 << chunk_len_log2) {
delta_bits = delta_bits.max(packed_bits(chunk.last().unwrap() - chunk[0]));
}
Parameters {
length: U64::new(values.len() as u64),
base_bits: packed_bits(last_value).max(1),
delta_bits,
chunk_len_log2,
}
})
.filter(|parameters| DELTA_BITS_RANGE.contains(¶meters.delta_bits))
}
}
#[cfg(feature = "testing")]
pub fn gen_test_sequence(rng: &mut impl rand::Rng, max_delta: u64, len: usize) -> Vec<u64> {
let mut last = 0u64;
(0..len)
.map(|_| {
use rand::RngExt;
last = last.checked_add(rng.random_range(0..=max_delta)).unwrap();
last
})
.collect()
}
#[cfg(test)]
mod tests {
use std::iter::{once, once_with};
use rand::rngs::StdRng;
use rand::{RngExt, SeedableRng};
use super::*;
#[test]
fn test_compress_decompress() {
for values in test_sequences() {
for parameters in Parameters::try_all(&values) {
let compressed = compress_with_parameters(&values, parameters);
let (decompressor, bytes) = Reader::new(parameters, &compressed).unwrap();
assert!(bytes.is_empty());
assert_eq!(decompressor.len(), values.len());
for (i, &value) in values.iter().enumerate() {
assert_eq!(decompressor.get(i), Some(value));
}
assert_eq!(decompressor.get(values.len()), None);
}
}
}
fn test_sequences() -> impl Iterator<Item = Vec<u64>> {
let params = [
(10, 1_000),
(20, 10_000), (10_000_000, 10_000), (0x123456789AB, 1_000), ];
itertools::chain!(
once(vec![]),
once(vec![0]),
once(vec![1]),
once(vec![u64::MAX]),
once(vec![u64::MAX, u64::MAX]),
once(vec![0, u64::MAX]), params.into_iter().map(|(max_delta, len)| {
gen_test_sequence(&mut StdRng::seed_from_u64(42), max_delta, len)
}),
once_with(|| {
let mut rng = StdRng::seed_from_u64(42);
let mut values = (0..1000).map(|_| rng.random()).collect::<Vec<_>>();
values.sort_unstable();
values
}),
)
}
}