use crate::groupcompress::delta::{apply_delta, read_base128_int, read_instruction, Instruction};
use byteorder::ReadBytesExt;
use std::borrow::Cow;
use std::io::BufRead;
use std::io::{Read, Write};
const GCB_HEADER: &[u8] = b"gcb1z\n";
const GCB_LZ_HEADER: &[u8] = b"gcb1l\n";
#[derive(Debug, PartialEq, Eq, Default, Clone, Copy)]
pub enum CompressorKind {
#[default]
Zlib,
Lzma,
}
#[cfg(feature = "pyo3")]
impl<'a, 'py> pyo3::FromPyObject<'a, 'py> for CompressorKind {
type Error = pyo3::PyErr;
fn extract(ob: pyo3::Borrowed<'a, 'py, pyo3::PyAny>) -> pyo3::PyResult<Self> {
let s: Cow<str> = ob.extract()?;
match s.as_ref() {
"zlib" => Ok(CompressorKind::Zlib),
"lzma" => Ok(CompressorKind::Lzma),
_ => Err(pyo3::exceptions::PyValueError::new_err(format!(
"Unknown compressor: {}",
s
))),
}
}
}
impl CompressorKind {
fn header(&self) -> &'static [u8] {
match self {
CompressorKind::Zlib => GCB_HEADER,
CompressorKind::Lzma => GCB_LZ_HEADER,
}
}
fn from_header(header: &[u8]) -> Option<Self> {
if header == GCB_HEADER {
Some(CompressorKind::Zlib)
} else if header == GCB_LZ_HEADER {
Some(CompressorKind::Lzma)
} else {
None
}
}
}
#[derive(Debug)]
pub enum Error {
InvalidData(String),
Io(std::io::Error),
}
impl From<std::io::Error> for Error {
fn from(e: std::io::Error) -> Self {
Error::Io(e)
}
}
impl From<super::delta::DeltaError> for Error {
fn from(e: super::delta::DeltaError) -> Self {
match e {
super::delta::DeltaError::Io { kind, ref message } => {
Error::Io(std::io::Error::new(kind, message.clone()))
}
other => Error::InvalidData(other.to_string()),
}
}
}
impl std::fmt::Display for Error {
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
match *self {
Error::InvalidData(ref s) => write!(f, "Invalid data: {}", s),
Error::Io(ref e) => write!(f, "IO error: {}", e),
}
}
}
impl std::error::Error for Error {}
pub enum GroupCompressItem {
Fulltext(Vec<u8>),
Delta(Vec<u8>),
}
pub fn read_item<R: Read>(r: &mut R) -> Result<GroupCompressItem, Error> {
let c = r.read_u8()?;
let content_len = read_base128_int(r).map_err(|e| Error::InvalidData(e.to_string()))?;
let mut text = vec![0; content_len as usize];
r.read_exact(&mut text)?;
match c {
b'f' => {
Ok(GroupCompressItem::Fulltext(text))
}
b'd' => {
Ok(GroupCompressItem::Delta(text))
}
c => Err(Error::InvalidData(format!(
"Unknown content control code: {:?}",
c
))),
}
}
enum Decompressor {
Lzma(xz2::read::XzDecoder<crate::osutils::chunkreader::ChunksReader<Vec<u8>>>),
Zlib(flate2::read::ZlibDecoder<crate::osutils::chunkreader::ChunksReader<Vec<u8>>>),
}
impl std::io::Read for Decompressor {
fn read(&mut self, buf: &mut [u8]) -> std::io::Result<usize> {
match self {
Decompressor::Lzma(d) => d.read(buf),
Decompressor::Zlib(d) => d.read(buf),
}
}
}
pub struct GroupCompressBlock {
compressor: Option<CompressorKind>,
z_content_chunks: Option<Vec<Vec<u8>>>,
z_content_decompressor: Option<Decompressor>,
z_content_length: Option<usize>,
content_length: Option<usize>,
content: Option<Vec<u8>>,
content_chunks: Option<Vec<Vec<u8>>>,
}
impl Default for GroupCompressBlock {
fn default() -> Self {
Self::new()
}
}
fn read_header<R: Read>(r: &mut R) -> Result<CompressorKind, Error> {
let mut header = [0; 6];
r.read_exact(&mut header).map_err(|e| {
Error::InvalidData(format!(
"Failed to read header from GroupCompressBlock: {}",
e
))
})?;
CompressorKind::from_header(&header).ok_or_else(|| {
Error::InvalidData(format!(
"Invalid header in GroupCompressBlock: {:?}",
header
))
})
}
impl GroupCompressBlock {
pub fn new() -> Self {
Self {
compressor: None,
z_content_chunks: None,
z_content_decompressor: None,
z_content_length: None,
content_length: None,
content: None,
content_chunks: None,
}
}
pub fn content(&self) -> Option<&[u8]> {
self.content.as_deref()
}
pub fn content_length(&self) -> Option<usize> {
self.content_length
}
pub fn z_content_length(&self) -> Option<usize> {
self.z_content_length
}
pub fn has_z_content_decompressor(&self) -> bool {
self.z_content_decompressor.is_some()
}
pub fn compressor(&self) -> Option<CompressorKind> {
self.compressor
}
pub fn set_compressor(&mut self, kind: CompressorKind) {
self.compressor = Some(kind);
self.content = None;
self.z_content_decompressor = None;
}
pub fn set_z_content_chunks(&mut self, chunks: Vec<Vec<u8>>) {
self.z_content_chunks = Some(chunks);
self.content = None;
self.z_content_decompressor = None;
}
pub fn set_z_content_length(&mut self, length: usize) {
self.z_content_length = Some(length);
}
pub fn set_content_length(&mut self, length: usize) {
self.content_length = Some(length);
}
pub fn ensure_content(&mut self, num_bytes: Option<usize>) -> Result<(), Error> {
let content_length = self
.content_length
.ok_or_else(|| Error::InvalidData("ensure_content: content_length not set".into()))?;
let mut num_bytes = match num_bytes {
None => content_length,
Some(num_bytes) => {
if num_bytes > content_length {
return Err(Error::InvalidData(format!(
"ensure_content: requested {} bytes but content length is {}",
num_bytes, content_length
)));
}
num_bytes
}
};
if self.content.is_none() {
if let Some(content_chunks) = self.content_chunks.as_ref() {
self.content = Some(content_chunks.concat());
self.content_chunks = None;
}
}
if self.content.is_none() {
if self.z_content_length == Some(0) {
self.content = Some(b"".to_vec());
} else {
let c = crate::osutils::chunkreader::ChunksReader::new(Box::new(
self.z_content_chunks.clone().unwrap().into_iter(),
));
self.z_content_decompressor = Some(match self.compressor.unwrap() {
CompressorKind::Lzma => Decompressor::Lzma(xz2::read::XzDecoder::new(c)),
CompressorKind::Zlib => Decompressor::Zlib(flate2::read::ZlibDecoder::new(c)),
});
self.content = Some(Vec::new());
}
}
if self.content.as_ref().unwrap().len() >= num_bytes {
if self.content.as_ref().unwrap().len() >= self.content_length.unwrap_or(0) {
self.z_content_decompressor = None;
}
return Ok(());
}
num_bytes -= self.content.as_ref().unwrap().len();
let mut buf = vec![0; num_bytes];
self.z_content_decompressor
.as_mut()
.unwrap()
.read_exact(&mut buf)?;
self.content.as_mut().unwrap().extend(buf);
if self.content.as_ref().unwrap().len() >= self.content_length.unwrap_or(0) {
self.z_content_decompressor = None;
}
Ok(())
}
#[allow(clippy::len_without_is_empty)]
pub fn len(&self) -> usize {
self.content_length.unwrap_or(0) + self.z_content_length.unwrap_or(0)
}
pub fn parse_bytes(&mut self, mut data: &[u8]) -> Result<(), Error> {
self.read_bytes(&mut data)
}
fn read_bytes<R: Read>(&mut self, r: &mut R) -> Result<(), Error> {
let mut buf = std::io::BufReader::new(r);
let mut z_content_length_buf = Vec::new();
buf.read_until(b'\n', &mut z_content_length_buf)?;
z_content_length_buf.pop();
let z_content_length: usize = String::from_utf8(z_content_length_buf)
.map_err(|e| Error::InvalidData(format!("z_content_length not UTF-8: {}", e)))?
.parse()
.map_err(|e| Error::InvalidData(format!("invalid z_content_length: {}", e)))?;
self.z_content_length = Some(z_content_length);
let mut content_length_buf = Vec::new();
buf.read_until(b'\n', &mut content_length_buf)?;
content_length_buf.pop();
let content_length: usize = String::from_utf8(content_length_buf)
.map_err(|e| Error::InvalidData(format!("content_length not UTF-8: {}", e)))?
.parse()
.map_err(|e| Error::InvalidData(format!("invalid content_length: {}", e)))?;
self.content_length = Some(content_length);
let mut data = Vec::new();
buf.read_to_end(&mut data)?;
if data.len() != z_content_length {
return Err(Error::InvalidData(format!(
"compressed body length mismatch: got {} bytes, header says {}",
data.len(),
z_content_length
)));
}
self.z_content_chunks = Some(vec![data.to_vec()]);
Ok(())
}
pub fn z_content(&mut self) -> Vec<u8> {
self.z_content_chunks.as_ref().unwrap().concat()
}
pub fn z_content_chunks(&mut self) -> &mut Vec<Vec<u8>> {
self.z_content_chunks.as_mut().unwrap()
}
pub fn from_bytes<R: Read>(mut r: R) -> Result<Self, Error> {
let compressor = read_header(&mut r)?;
let mut out = Self {
compressor: Some(compressor),
z_content_chunks: None,
content: None,
content_chunks: None,
z_content_length: None,
content_length: None,
z_content_decompressor: None,
};
out.read_bytes(&mut r)?;
Ok(out)
}
pub fn extract(&mut self, start: usize, end: usize) -> Result<Vec<Vec<u8>>, Error> {
if start == 0 && end == 0 {
return Ok(vec![]);
}
self.ensure_content(Some(end))?;
let content = self.content.as_ref().unwrap();
if end > content.len() || start >= end {
return Err(Error::InvalidData(format!(
"extract range {}..{} out of bounds for content of length {}",
start,
end,
content.len()
)));
}
let mut record = &content[start..end];
match read_item(&mut record)? {
GroupCompressItem::Fulltext(data) => Ok(vec![data]),
GroupCompressItem::Delta(delta) => {
let reconstructed = apply_delta(content, delta.as_slice())?;
Ok(vec![reconstructed])
}
}
}
pub fn set_chunked_content(&mut self, content_chunks: &[Vec<u8>], length: usize) {
self.content_length = Some(length);
self.content_chunks = Some(content_chunks.to_vec());
self.content = None;
self.z_content_chunks = None;
}
pub fn set_content(&mut self, content: &[u8]) {
self.content_length = Some(content.len());
self.content = Some(content.to_vec());
self.z_content_chunks = None;
}
fn create_z_content_from_chunks(
&mut self,
chunks: Vec<Vec<u8>>,
compressor_kind: CompressorKind,
) {
let chunks = match compressor_kind {
CompressorKind::Zlib => {
let mut encoder =
flate2::write::ZlibEncoder::new(Vec::new(), flate2::Compression::default());
for chunk in chunks {
encoder.write_all(&chunk).unwrap();
}
encoder.finish().unwrap()
}
CompressorKind::Lzma => {
let mut encoder = xz2::write::XzEncoder::new(Vec::new(), 6);
for chunk in chunks {
encoder.write_all(&chunk).unwrap();
}
encoder.finish().unwrap()
}
};
self.z_content_length = Some(chunks.len());
self.z_content_chunks = Some(vec![chunks]);
}
fn create_z_content(&mut self, compressor_kind: CompressorKind) {
if self.z_content_chunks.is_some() && self.compressor == Some(compressor_kind) {
return;
}
let chunks = if let Some(content_chunks) = self.content_chunks.as_ref() {
content_chunks.to_vec()
} else {
vec![self.content.as_ref().unwrap().clone()]
};
self.create_z_content_from_chunks(chunks, compressor_kind);
}
pub fn to_chunks(
&mut self,
compressor_kind: Option<CompressorKind>,
) -> (usize, Vec<Cow<'_, [u8]>>) {
let compressor_kind = compressor_kind.unwrap_or_default();
self.create_z_content(compressor_kind);
let mut header_chunk = compressor_kind.header().to_vec();
header_chunk.extend_from_slice(
format!(
"{}\n{}\n",
self.z_content_length.unwrap(),
self.content_length.unwrap()
)
.as_bytes(),
);
let mut chunks: Vec<Cow<'_, [u8]>> = vec![Cow::Owned(header_chunk)];
chunks.extend(
self.z_content_chunks
.as_ref()
.unwrap()
.iter()
.map(|x| Cow::Borrowed(x.as_slice())),
);
let total_len = chunks.iter().map(|x| x.len()).sum();
(total_len, chunks)
}
pub fn to_bytes(&mut self) -> Vec<u8> {
let (_total_len, chunks) = self.to_chunks(None);
chunks.concat()
}
pub fn dump(&mut self, include_text: Option<bool>) -> Result<Vec<DumpInfo>, Error> {
let include_text = include_text.unwrap_or(false);
self.ensure_content(None)?;
let mut result = vec![];
let mut content = self.content.as_ref().unwrap().as_slice();
while !content.is_empty() {
match read_item(&mut content)? {
GroupCompressItem::Fulltext(text) => {
let length = text.len();
result.push(DumpInfo::Fulltext {
length,
text: if include_text { Some(text) } else { None },
});
}
GroupCompressItem::Delta(delta_content) => {
let delta_length = delta_content.len();
let mut delta_info = vec![];
let mut delta_slice = delta_content.as_slice();
let decomp_len = read_base128_int(&mut delta_slice).unwrap();
let mut measured_len = 0;
while !delta_slice.is_empty() {
match read_instruction(&mut delta_slice)? {
Instruction::Insert(text) => {
measured_len += text.len();
delta_info.push(DeltaInfo::Insert {
length: text.len(),
text: if include_text { Some(text) } else { None },
});
}
Instruction::r#Copy { offset, length } => {
delta_info.push(DeltaInfo::Copy {
offset,
length,
text: if include_text {
Some(
self.content.as_ref().unwrap()[offset..offset + length]
.to_vec(),
)
} else {
None
},
});
measured_len += length;
}
}
}
if measured_len != decomp_len as usize {
return Err(Error::InvalidData(format!(
"Delta claimed fulltext was {} bytes, but extraction resulted in {}",
decomp_len, measured_len
)));
}
result.push(DumpInfo::Delta {
delta_length,
decomp_length: decomp_len as usize,
instructions: delta_info,
});
}
}
}
Ok(result)
}
}
pub enum DeltaInfo {
Insert {
length: usize,
text: Option<Vec<u8>>,
},
Copy {
offset: usize,
length: usize,
text: Option<Vec<u8>>,
},
}
pub enum DumpInfo {
Fulltext {
length: usize,
text: Option<Vec<u8>>,
},
Delta {
delta_length: usize,
decomp_length: usize,
instructions: Vec<DeltaInfo>,
},
}
#[cfg(test)]
mod tests {
use super::*;
use crate::groupcompress::delta::write_base128_int;
fn make_fulltext_record(body: &[u8]) -> Vec<u8> {
let mut out = vec![b'f'];
write_base128_int(&mut out, body.len() as u128).unwrap();
out.extend_from_slice(body);
out
}
#[test]
fn compressor_kind_header_round_trip() {
assert_eq!(
CompressorKind::from_header(GCB_HEADER),
Some(CompressorKind::Zlib)
);
assert_eq!(
CompressorKind::from_header(GCB_LZ_HEADER),
Some(CompressorKind::Lzma)
);
assert_eq!(CompressorKind::from_header(b"xxxxxx"), None);
assert_eq!(CompressorKind::Zlib.header(), GCB_HEADER);
assert_eq!(CompressorKind::Lzma.header(), GCB_LZ_HEADER);
}
#[test]
fn new_block_has_no_content() {
let b = GroupCompressBlock::new();
assert!(b.content().is_none());
assert!(b.content_length().is_none());
}
#[test]
fn new_block_len_is_zero() {
let b = GroupCompressBlock::new();
assert_eq!(b.len(), 0);
}
#[test]
fn manually_initialised_block_decompresses() {
use flate2::write::ZlibEncoder;
use std::io::Write;
let body: Vec<u8> = b"partial decomp target content\n".repeat(50);
let mut encoder = ZlibEncoder::new(Vec::new(), flate2::Compression::default());
encoder.write_all(&body).unwrap();
let z_content = encoder.finish().unwrap();
let mut b = GroupCompressBlock::new();
b.set_z_content_chunks(vec![z_content.clone()]);
b.set_z_content_length(z_content.len());
b.set_compressor(CompressorKind::Zlib);
b.set_content_length(body.len());
assert!(b.content().is_none());
b.ensure_content(Some(100)).unwrap();
assert!(b.content().unwrap().len() >= 100);
assert_eq!(&b.content().unwrap()[..100], &body[..100]);
b.ensure_content(None).unwrap();
assert_eq!(b.content(), Some(body.as_slice()));
}
#[test]
fn len_after_set_content_reports_content_length() {
let body = b"abc\n";
let mut b = GroupCompressBlock::new();
b.set_content(body);
assert!(b.len() >= body.len());
}
#[test]
fn set_content_round_trip_via_to_bytes_and_from_bytes() {
let body = b"hello world\nthis is a single fulltext\n";
let record = make_fulltext_record(body);
let mut b = GroupCompressBlock::new();
b.set_content(&record);
let raw = b.to_bytes();
assert!(raw.starts_with(GCB_HEADER));
let mut parsed = GroupCompressBlock::from_bytes(raw.as_slice()).unwrap();
assert_eq!(parsed.content_length(), Some(record.len()));
parsed.ensure_content(None).unwrap();
assert_eq!(parsed.content(), Some(record.as_slice()));
}
#[test]
fn set_chunked_content_matches_set_content_on_the_wire() {
let part1: Vec<u8> = b"hello world\n".to_vec();
let part2: Vec<u8> = b"more content\n".to_vec();
let total_len = part1.len() + part2.len();
let mut chunked = GroupCompressBlock::new();
chunked.set_chunked_content(&[part1.clone(), part2.clone()], total_len);
let chunked_bytes = chunked.to_bytes();
let mut flat = GroupCompressBlock::new();
let mut combined = part1.clone();
combined.extend_from_slice(&part2);
flat.set_content(&combined);
let flat_bytes = flat.to_bytes();
assert_eq!(chunked_bytes, flat_bytes);
}
#[test]
fn ensure_content_full_decompression_recovers_original() {
let body: Vec<u8> = b"line of reasonably compressible text\n".repeat(200);
let record = make_fulltext_record(&body);
let mut src = GroupCompressBlock::new();
src.set_content(&record);
let raw = src.to_bytes();
let mut parsed = GroupCompressBlock::from_bytes(raw.as_slice()).unwrap();
assert!(parsed.content().is_none());
parsed.ensure_content(None).unwrap();
assert_eq!(parsed.content(), Some(record.as_slice()));
}
#[test]
fn ensure_content_partial_then_full() {
let body: Vec<u8> = b"compressible line\n".repeat(100);
let record = make_fulltext_record(&body);
let mut src = GroupCompressBlock::new();
src.set_content(&record);
let raw = src.to_bytes();
let mut parsed = GroupCompressBlock::from_bytes(raw.as_slice()).unwrap();
parsed.ensure_content(Some(50)).unwrap();
assert!(parsed.content().unwrap().len() >= 50);
parsed.ensure_content(None).unwrap();
assert_eq!(parsed.content(), Some(record.as_slice()));
}
#[test]
fn to_chunks_produces_header_plus_lengths_chunk_then_payload() {
let body = b"some body text\n";
let record = make_fulltext_record(body);
let mut b = GroupCompressBlock::new();
b.set_content(&record);
let (total_len, chunks) = b.to_chunks(None);
assert!(chunks[0].starts_with(GCB_HEADER));
let tail = std::str::from_utf8(&chunks[0][GCB_HEADER.len()..]).unwrap();
let mut iter = tail.trim_end().split('\n');
let z_len: usize = iter.next().unwrap().parse().unwrap();
let u_len: usize = iter.next().unwrap().parse().unwrap();
assert_eq!(u_len, record.len());
let payload_len: usize = chunks[1..].iter().map(|c| c.len()).sum();
assert_eq!(payload_len, z_len);
assert_eq!(total_len, chunks.iter().map(|c| c.len()).sum::<usize>());
}
#[test]
fn dump_reports_fulltext_records_without_text_by_default() {
let body_a = b"first body\n";
let body_b = b"second body\n";
let mut content = make_fulltext_record(body_a);
content.extend(make_fulltext_record(body_b));
let mut b = GroupCompressBlock::new();
b.set_content(&content);
let dump = b.dump(None).unwrap();
assert_eq!(dump.len(), 2);
assert!(matches!(dump[0], DumpInfo::Fulltext { text: None, .. }));
assert!(matches!(dump[1], DumpInfo::Fulltext { text: None, .. }));
}
#[test]
fn ensure_content_drops_decompressor_on_full_drain() {
let body: Vec<u8> = b"ensurable content here\n".repeat(100);
let record = make_fulltext_record(&body);
let mut src = GroupCompressBlock::new();
src.set_content(&record);
let raw = src.to_bytes();
let mut parsed = GroupCompressBlock::from_bytes(raw.as_slice()).unwrap();
parsed.ensure_content(Some(50)).unwrap();
assert!(parsed.has_z_content_decompressor());
parsed.ensure_content(None).unwrap();
assert!(!parsed.has_z_content_decompressor());
}
#[test]
fn ensure_content_is_idempotent() {
let body: Vec<u8> = b"some compressible content\n".repeat(200);
let record = make_fulltext_record(&body);
let mut src = GroupCompressBlock::new();
src.set_content(&record);
let raw = src.to_bytes();
let mut parsed = GroupCompressBlock::from_bytes(raw.as_slice()).unwrap();
parsed.ensure_content(None).unwrap();
let first = parsed.content().unwrap().to_vec();
parsed.ensure_content(None).unwrap();
assert_eq!(parsed.content().unwrap(), first.as_slice());
parsed.ensure_content(Some(10)).unwrap();
assert_eq!(parsed.content().unwrap(), first.as_slice());
}
#[test]
fn extract_reads_record_at_given_start_offset() {
let body_a = b"first body\n";
let body_b = b"second body\n";
let rec_a = make_fulltext_record(body_a);
let rec_b = make_fulltext_record(body_b);
let mut content = rec_a.clone();
content.extend_from_slice(&rec_b);
let mut b = GroupCompressBlock::new();
b.set_content(&content);
let start_a = 0;
let end_a = rec_a.len();
let out_a = b.extract(start_a, end_a).unwrap();
assert_eq!(out_a, vec![body_a.to_vec()]);
let start_b = rec_a.len();
let end_b = content.len();
let out_b = b.extract(start_b, end_b).unwrap();
assert_eq!(out_b, vec![body_b.to_vec()]);
}
#[test]
fn dump_with_include_text_returns_payload() {
let body = b"included body\n";
let content = make_fulltext_record(body);
let mut b = GroupCompressBlock::new();
b.set_content(&content);
let dump = b.dump(Some(true)).unwrap();
assert_eq!(dump.len(), 1);
match &dump[0] {
DumpInfo::Fulltext {
text: Some(text), ..
} => assert_eq!(text.as_slice(), body),
_ => panic!("expected Fulltext with text"),
}
}
#[test]
fn dump_reports_delta_records_with_instructions() {
use crate::groupcompress::compressor::{GroupCompressor, RabinGroupCompressor};
use crate::versionedfile::Key;
let mut gc = RabinGroupCompressor::new(None);
let base = b"shared content that is long enough for rabin matching\nmore shared\n";
let derived = b"shared content that is long enough for rabin matching\nmore shared\nplus\n";
gc.compress(
&Key::Fixed(vec![b"base".to_vec()]),
&[base.as_slice()],
base.len(),
None,
None,
None,
)
.unwrap();
gc.compress(
&Key::Fixed(vec![b"derived".to_vec()]),
&[derived.as_slice()],
derived.len(),
None,
None,
None,
)
.unwrap();
let (chunks, endpoint) = gc.flush();
let mut b = GroupCompressBlock::new();
b.set_chunked_content(&chunks, endpoint);
let dump = b.dump(None).unwrap();
assert_eq!(dump.len(), 2);
match &dump[0] {
DumpInfo::Fulltext { length, text: None } => assert_eq!(*length, base.len()),
_ => panic!(
"expected Fulltext(None) for first record, got {:?}",
match_kind(&dump[0])
),
}
match &dump[1] {
DumpInfo::Delta {
decomp_length,
instructions,
..
} => {
assert_eq!(*decomp_length, derived.len());
assert!(!instructions.is_empty());
let (copies, inserts): (usize, usize) =
instructions.iter().fold((0, 0), |(c, i), inst| match inst {
DeltaInfo::Copy { .. } => (c + 1, i),
DeltaInfo::Insert { .. } => (c, i + 1),
});
assert!(copies >= 1, "delta should contain at least one copy");
assert!(inserts >= 1, "delta should contain at least one insert");
}
_ => panic!("expected Delta for second record"),
}
}
fn match_kind(info: &DumpInfo) -> &'static str {
match info {
DumpInfo::Fulltext { .. } => "Fulltext",
DumpInfo::Delta { .. } => "Delta",
}
}
#[test]
fn z_content_length_reflects_setter() {
let mut b = GroupCompressBlock::new();
assert_eq!(b.z_content_length(), None);
b.set_z_content_length(1234);
assert_eq!(b.z_content_length(), Some(1234));
}
#[test]
fn compressor_getter_and_setter_round_trip() {
let mut b = GroupCompressBlock::new();
assert_eq!(b.compressor(), None);
b.set_compressor(CompressorKind::Zlib);
assert_eq!(b.compressor(), Some(CompressorKind::Zlib));
b.set_compressor(CompressorKind::Lzma);
assert_eq!(b.compressor(), Some(CompressorKind::Lzma));
}
#[test]
fn set_z_content_chunks_clears_cached_content() {
use flate2::write::ZlibEncoder;
use std::io::Write;
let body: Vec<u8> = b"the original body bytes\n".repeat(20);
let rec = make_fulltext_record(&body);
let mut src = GroupCompressBlock::new();
src.set_content(&rec);
src.to_bytes();
let mut parsed = GroupCompressBlock::from_bytes(src.to_bytes().as_slice()).unwrap();
parsed.ensure_content(None).unwrap();
assert!(parsed.content().is_some());
let replacement_body: Vec<u8> = b"replacement body bytes\n".repeat(20);
let replacement_record = make_fulltext_record(&replacement_body);
let mut encoder = ZlibEncoder::new(Vec::new(), flate2::Compression::default());
encoder.write_all(&replacement_record).unwrap();
let z_replacement = encoder.finish().unwrap();
parsed.set_compressor(CompressorKind::Zlib);
parsed.set_z_content_chunks(vec![z_replacement.clone()]);
parsed.set_z_content_length(z_replacement.len());
parsed.set_content_length(replacement_record.len());
assert!(
parsed.content().is_none(),
"cached content should be cleared"
);
parsed.ensure_content(None).unwrap();
assert_eq!(parsed.content(), Some(replacement_record.as_slice()));
}
#[test]
fn lzma_round_trip_via_to_chunks_from_bytes() {
let body: Vec<u8> = b"a bit of compressible lzma-bound text\n".repeat(40);
let record = make_fulltext_record(&body);
let mut src = GroupCompressBlock::new();
src.set_content(&record);
let (_total_len, chunks) = src.to_chunks(Some(CompressorKind::Lzma));
let bytes: Vec<u8> = chunks.iter().flat_map(|c| c.iter().copied()).collect();
assert!(bytes.starts_with(GCB_LZ_HEADER));
let mut parsed = GroupCompressBlock::from_bytes(bytes.as_slice()).unwrap();
assert_eq!(parsed.compressor(), Some(CompressorKind::Lzma));
parsed.ensure_content(None).unwrap();
assert_eq!(parsed.content(), Some(record.as_slice()));
}
}