use crate::groupcompress::block::{read_item, GroupCompressItem};
use crate::groupcompress::delta::{apply_delta, read_base128_int, write_base128_int, DeltaError};
use crate::groupcompress::rabin_delta::OwningDeltaIndex;
use crate::groupcompress::NULL_SHA1;
use crate::versionedfile::{Error, Key};
use std::borrow::Cow;
use std::collections::HashMap;
use std::sync::Mutex;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum RecordKind {
Fulltext,
Delta,
}
impl RecordKind {
pub fn as_str(self) -> &'static str {
match self {
RecordKind::Fulltext => "fulltext",
RecordKind::Delta => "delta",
}
}
}
#[derive(Debug)]
pub enum ExtractError {
KeyNotFound(Vec<Vec<u8>>),
EmptyPayload,
LengthMismatch { stored: usize, expected: usize },
UnknownKind(u8),
BlockRead(super::block::Error),
BadLengthPrefix(String),
DeltaApply(DeltaError),
}
impl std::fmt::Display for ExtractError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
ExtractError::KeyNotFound(key) => write!(f, "key not found in compressor: {:?}", key),
ExtractError::EmptyPayload => write!(f, "empty stored bytes"),
ExtractError::LengthMismatch { stored, expected } => write!(
f,
"Index claimed length, but stored bytes claim {} != {}",
stored, expected
),
ExtractError::UnknownKind(b) => {
write!(f, "Unknown content kind, bytes claim {}", *b as char)
}
ExtractError::BlockRead(e) => write!(f, "{}", e),
ExtractError::BadLengthPrefix(s) => write!(f, "{}", s),
ExtractError::DeltaApply(e) => write!(f, "{}", e),
}
}
}
impl std::error::Error for ExtractError {}
impl From<super::block::Error> for ExtractError {
fn from(e: super::block::Error) -> Self {
ExtractError::BlockRead(e)
}
}
impl From<DeltaError> for ExtractError {
fn from(e: DeltaError) -> Self {
ExtractError::DeltaApply(e)
}
}
pub trait GroupCompressor {
fn compress(
&mut self,
key: &Key,
chunks: &[&[u8]],
length: usize,
expected_sha: Option<String>,
nostore_sha: Option<String>,
soft: Option<bool>,
) -> Result<(String, usize, usize, RecordKind), Error> {
if length == 0 {
if nostore_sha == Some(String::from_utf8_lossy(NULL_SHA1.as_slice()).to_string()) {
return Err(Error::ExistingContent(key.clone()));
}
return Ok((
String::from_utf8_lossy(NULL_SHA1.as_slice()).to_string(),
0,
0,
RecordKind::Fulltext,
));
}
let sha = expected_sha.unwrap_or_else(|| crate::osutils::sha::sha_chunks(chunks));
if let Some(nostore_sha) = nostore_sha {
if sha == nostore_sha {
return Err(Error::ExistingContent(key.clone()));
}
}
let key = match key {
Key::Fixed(key) => key.clone(),
Key::ContentAddressed(key) => {
let mut key = key.clone();
key.push(format!("sha1:{}", sha).as_bytes().to_vec());
key
}
};
let (start, end, r#type) =
self.compress_block(key, chunks, length, (length / 2) as u128, soft)?;
Ok((sha, start, end, r#type))
}
fn compress_block(
&mut self,
key: Vec<Vec<u8>>,
chunks: &[&[u8]],
input_len: usize,
max_delta_size: u128,
soft: Option<bool>,
) -> Result<(usize, usize, RecordKind), Error>;
fn ratio(&self) -> f32;
fn flush(self) -> (Vec<Vec<u8>>, usize);
fn flush_without_last(self) -> (Vec<Vec<u8>>, usize);
}
pub struct TraditionalGroupCompressor {
delta_index: crate::groupcompress::line_delta::LinesDeltaIndex,
endpoint: usize,
input_bytes: usize,
last: Option<(usize, usize)>,
labels_deltas: HashMap<Vec<Vec<u8>>, (usize, usize, usize, usize)>,
}
impl GroupCompressor for TraditionalGroupCompressor {
fn ratio(&self) -> f32 {
if self.endpoint == 0 {
0.0
} else {
self.input_bytes as f32 / self.endpoint as f32
}
}
fn flush(self) -> (Vec<Vec<u8>>, usize) {
(self.delta_index.lines().to_vec(), self.endpoint)
}
fn flush_without_last(self) -> (Vec<Vec<u8>>, usize) {
let last = self.last.unwrap();
(self.delta_index.lines()[..last.0].to_vec(), last.1)
}
fn compress_block(
&mut self,
key: Vec<Vec<u8>>,
chunks: &[&[u8]],
input_len: usize,
max_delta_size: u128,
soft: Option<bool>,
) -> Result<(usize, usize, RecordKind), Error> {
let new_lines =
crate::osutils::chunks_to_lines(chunks.iter().map(|x| Ok::<_, std::io::Error>(*x)))
.collect::<Result<Vec<_>, _>>()
.unwrap();
let (mut out_lines, mut index_lines) =
self.delta_index
.make_delta(new_lines.as_slice(), input_len, soft);
let delta_length = out_lines.iter().map(|l| l.len() as u128).sum();
let (kind, out_lines) = if delta_length > max_delta_size {
let mut out_lines = vec![Cow::Borrowed(&b"f"[..]), {
let mut data = Vec::new();
write_base128_int(&mut data, input_len as u128).unwrap();
Cow::Owned(data)
}];
index_lines.clear();
index_lines.extend(vec![false, false]);
index_lines.extend([true].repeat(new_lines.len()));
out_lines.extend(new_lines);
(RecordKind::Fulltext, out_lines)
} else {
out_lines[0] = Cow::Borrowed(&b"d"[..]);
{
let mut data = Vec::new();
write_base128_int(&mut data, delta_length).unwrap();
out_lines[1] = Cow::Owned(data);
}
(RecordKind::Delta, out_lines)
};
let start = self.endpoint;
let chunk_start = self.delta_index.lines().len();
self.last = Some((chunk_start, self.endpoint));
self.delta_index.extend_lines(
out_lines
.into_iter()
.map(|x| x.into_owned())
.collect::<Vec<_>>()
.as_slice(),
&index_lines,
);
self.endpoint = self.delta_index.endpoint();
self.input_bytes += input_len;
let chunk_end = self.delta_index.lines().len();
self.labels_deltas
.insert(key, (start, chunk_start, self.endpoint, chunk_end));
Ok((start, self.endpoint, kind))
}
}
impl Default for TraditionalGroupCompressor {
fn default() -> Self {
Self::new()
}
}
impl TraditionalGroupCompressor {
pub fn new() -> Self {
Self {
delta_index: crate::groupcompress::line_delta::LinesDeltaIndex::new(vec![]),
endpoint: 0,
input_bytes: 0,
last: None,
labels_deltas: HashMap::new(),
}
}
pub fn chunks(&self) -> &[Vec<u8>] {
self.delta_index.lines()
}
pub fn endpoint(&self) -> usize {
self.endpoint
}
pub fn extract(&self, key: &[Vec<u8>]) -> Result<(Vec<Vec<u8>>, String), ExtractError> {
let (_start_byte, start_chunk, _end_byte, end_chunk) = self
.labels_deltas
.get(key)
.ok_or_else(|| ExtractError::KeyNotFound(key.to_vec()))?;
let delta_chunks = &self.delta_index.lines()[*start_chunk..*end_chunk];
let stored_bytes = delta_chunks.concat();
let data = match read_item(&mut stored_bytes.as_slice())? {
GroupCompressItem::Fulltext(data) => vec![data],
GroupCompressItem::Delta(data) => {
let source = self.delta_index.lines()[..*start_chunk].concat();
vec![apply_delta(source.as_slice(), data.as_slice())?]
}
};
let data_sha1 = crate::osutils::sha::sha_chunks(data.as_slice());
Ok((data, data_sha1))
}
}
pub struct RabinGroupCompressor {
delta_index: OwningDeltaIndex,
chunks: Vec<Vec<u8>>,
endpoint: usize,
input_bytes: usize,
last: Option<(usize, usize)>,
labels_deltas: HashMap<Vec<Vec<u8>>, (usize, usize, usize, usize)>,
prefix_cache: Mutex<(usize, Vec<u8>)>,
}
impl Default for RabinGroupCompressor {
fn default() -> Self {
Self::new(None)
}
}
impl RabinGroupCompressor {
pub fn new(max_bytes_to_index: Option<usize>) -> Self {
Self {
delta_index: OwningDeltaIndex::new(max_bytes_to_index),
chunks: Vec::new(),
endpoint: 0,
input_bytes: 0,
last: None,
labels_deltas: HashMap::new(),
prefix_cache: Mutex::new((0, Vec::new())),
}
}
fn source_prefix(&self, n: usize) -> Vec<u8> {
let mut cache = self.prefix_cache.lock().unwrap();
let (cached_n, buf) = &mut *cache;
if *cached_n > n {
buf.clear();
*cached_n = 0;
}
for chunk in &self.chunks[*cached_n..n] {
buf.extend_from_slice(chunk);
}
*cached_n = n;
buf.clone()
}
fn invalidate_prefix_cache(&mut self) {
let (cached_n, buf) = &mut *self.prefix_cache.lock().unwrap();
*cached_n = 0;
buf.clear();
}
pub fn chunks(&self) -> &[Vec<u8>] {
&self.chunks
}
pub fn endpoint(&self) -> usize {
self.endpoint
}
pub fn input_bytes(&self) -> usize {
self.input_bytes
}
pub fn max_bytes_to_index(&self) -> Option<usize> {
self.delta_index.max_bytes_to_index()
}
pub fn labels_deltas(&self) -> &HashMap<Vec<Vec<u8>>, (usize, usize, usize, usize)> {
&self.labels_deltas
}
pub fn extract(&self, key: &[Vec<u8>]) -> Result<(Vec<Vec<u8>>, String), ExtractError> {
let (_start_byte, start_chunk, _end_byte, end_chunk) = self
.labels_deltas
.get(key)
.ok_or_else(|| ExtractError::KeyNotFound(key.to_vec()))?;
let delta_chunks = &self.chunks[*start_chunk..*end_chunk];
let stored_bytes: Vec<u8> = delta_chunks.concat();
if stored_bytes.is_empty() {
return Err(ExtractError::EmptyPayload);
}
let kind = stored_bytes[0];
let mut cursor = std::io::Cursor::new(&stored_bytes[1..]);
let payload_len = read_base128_int(&mut cursor)
.map_err(|e| ExtractError::BadLengthPrefix(e.to_string()))?;
let len_len = cursor.position() as usize;
let data_len = payload_len as usize + 1 + len_len;
if data_len != stored_bytes.len() {
return Err(ExtractError::LengthMismatch {
stored: stored_bytes.len(),
expected: data_len,
});
}
let payload = &stored_bytes[1 + len_len..];
let data = match kind {
b'f' => vec![payload.to_vec()],
b'd' => {
let source = self.source_prefix(*start_chunk);
vec![apply_delta(&source, payload)?]
}
other => return Err(ExtractError::UnknownKind(other)),
};
let data_sha1 = crate::osutils::sha::sha_chunks(&data);
Ok((data, data_sha1))
}
fn output_chunks(&mut self, new_chunks: Vec<Vec<u8>>) {
self.last = Some((self.chunks.len(), self.endpoint));
let added: usize = new_chunks.iter().map(|c| c.len()).sum();
self.chunks.extend(new_chunks);
self.endpoint += added;
}
pub fn pop_last(&mut self) {
let (chunk_start, byte_endpoint) = self.last.expect("pop_last called without a last entry");
self.chunks.truncate(chunk_start);
self.endpoint = byte_endpoint;
self.last = None;
self.invalidate_prefix_cache();
}
}
impl GroupCompressor for RabinGroupCompressor {
fn ratio(&self) -> f32 {
if self.endpoint == 0 {
0.0
} else {
self.input_bytes as f32 / self.endpoint as f32
}
}
fn flush(self) -> (Vec<Vec<u8>>, usize) {
(self.chunks, self.endpoint)
}
fn flush_without_last(mut self) -> (Vec<Vec<u8>>, usize) {
self.pop_last();
self.flush()
}
fn compress_block(
&mut self,
key: Vec<Vec<u8>>,
chunks: &[&[u8]],
input_len: usize,
max_delta_size: u128,
_soft: Option<bool>,
) -> Result<(usize, usize, RecordKind), Error> {
let max_delta = max_delta_size as usize;
let owned_input: Option<Vec<u8>> = if chunks.len() == 1 {
None
} else {
Some(chunks.iter().flat_map(|c| c.iter().copied()).collect())
};
let delta = {
let input: &[u8] = owned_input.as_deref().unwrap_or(chunks[0]);
self.delta_index
.make_delta(input, max_delta)
.expect("rabin delta indexing")
};
let (kind, new_chunks): (RecordKind, Vec<Vec<u8>>) = match delta {
None => {
let mut enc_length = Vec::new();
write_base128_int(&mut enc_length, input_len as u128).unwrap();
let len_mini_header = 1 + enc_length.len();
let bytes = owned_input.unwrap_or_else(|| chunks[0].to_vec());
self.delta_index.add_source(bytes, len_mini_header);
let mut new_chunks = Vec::with_capacity(2 + chunks.len());
new_chunks.push(b"f".to_vec());
new_chunks.push(enc_length);
for chunk in chunks {
new_chunks.push(chunk.to_vec());
}
(RecordKind::Fulltext, new_chunks)
}
Some(delta_bytes) => {
let mut enc_length = Vec::new();
write_base128_int(&mut enc_length, delta_bytes.len() as u128).unwrap();
let len_mini_header = 1 + enc_length.len();
self.delta_index
.add_delta_source(delta_bytes.clone(), len_mini_header)
.expect("rabin delta source");
let new_chunks = vec![b"d".to_vec(), enc_length, delta_bytes];
(RecordKind::Delta, new_chunks)
}
};
let start = self.endpoint;
let chunk_start = self.chunks.len();
self.output_chunks(new_chunks);
self.input_bytes += input_len;
let chunk_end = self.chunks.len();
self.labels_deltas
.insert(key, (start, chunk_start, self.endpoint, chunk_end));
Ok((start, self.endpoint, kind))
}
}
#[cfg(test)]
mod tests {
use super::*;
fn key(parts: &[&[u8]]) -> Key {
Key::Fixed(parts.iter().map(|p| p.to_vec()).collect())
}
#[test]
fn rabin_compressor_round_trips_fulltext() {
let mut gc = RabinGroupCompressor::new(None);
let text = b"hello world\nthis is a fulltext\n";
let (sha, start, end, kind) = gc
.compress(
&key(&[b"label"]),
&[text.as_slice()],
text.len(),
None,
None,
None,
)
.unwrap();
assert_eq!(kind, RecordKind::Fulltext);
assert!(end > start);
assert!(!sha.is_empty());
let stored_key: Vec<Vec<u8>> = vec![b"label".to_vec()];
let (data, data_sha) = gc.extract(&stored_key).unwrap();
assert_eq!(data, vec![text.to_vec()]);
assert_eq!(data_sha, sha);
}
#[test]
fn rabin_compressor_round_trips_delta() {
let mut gc = RabinGroupCompressor::new(None);
let base = b"common prefix that is long enough to be worth indexing\nmore shared text\n";
let derived = b"common prefix that is long enough to be worth indexing\nmore shared text\nplus a little extra\n";
gc.compress(
&key(&[b"base"]),
&[base.as_slice()],
base.len(),
None,
None,
None,
)
.unwrap();
let (_sha, _start, _end, kind) = gc
.compress(
&key(&[b"derived"]),
&[derived.as_slice()],
derived.len(),
None,
None,
None,
)
.unwrap();
assert_eq!(kind, RecordKind::Delta);
let (data, _) = gc.extract(&vec![b"derived".to_vec()]).unwrap();
assert_eq!(data, vec![derived.to_vec()]);
}
#[test]
fn rabin_compressor_pop_last_rolls_back() {
let mut gc = RabinGroupCompressor::new(None);
gc.compress(
&key(&[b"a"]),
&[b"first record\n".as_slice()],
13,
None,
None,
None,
)
.unwrap();
let chunks_after_first = gc.chunks().to_vec();
let endpoint_after_first = gc.endpoint();
gc.compress(
&key(&[b"b"]),
&[b"second record\n".as_slice()],
14,
None,
None,
None,
)
.unwrap();
gc.pop_last();
assert_eq!(gc.chunks(), chunks_after_first.as_slice());
assert_eq!(gc.endpoint(), endpoint_after_first);
}
#[test]
fn rabin_compressor_empty_input_returns_null_sha() {
let mut gc = RabinGroupCompressor::new(None);
let (sha, start, end, kind) = gc
.compress(&key(&[b"empty"]), &[], 0, None, None, None)
.unwrap();
assert_eq!(start, 0);
assert_eq!(end, 0);
assert_eq!(kind, RecordKind::Fulltext);
assert_eq!(sha.as_bytes(), crate::groupcompress::NULL_SHA1.as_slice());
assert_eq!(gc.endpoint(), 0);
assert!(gc.labels_deltas().is_empty());
}
#[test]
fn rabin_compressor_empty_input_with_matching_nostore_raises() {
let mut gc = RabinGroupCompressor::new(None);
let null_sha = String::from_utf8(crate::groupcompress::NULL_SHA1.clone()).unwrap();
let err = gc
.compress(&key(&[b"empty"]), &[], 0, None, Some(null_sha), None)
.unwrap_err();
assert!(matches!(err, Error::ExistingContent(_)));
}
#[test]
fn rabin_compressor_nostore_sha_match_raises_existing_content() {
let mut gc = RabinGroupCompressor::new(None);
let text = b"some content that we want to deduplicate\n";
let actual_sha = crate::osutils::sha::sha_chunks(&[text.as_slice()]);
let err = gc
.compress(
&key(&[b"label"]),
&[text.as_slice()],
text.len(),
None,
Some(actual_sha),
None,
)
.unwrap_err();
assert!(matches!(err, Error::ExistingContent(_)));
assert_eq!(gc.endpoint(), 0);
assert!(gc.labels_deltas().is_empty());
}
#[test]
fn rabin_compressor_expected_sha_passthrough() {
let mut gc = RabinGroupCompressor::new(None);
let text = b"a small fulltext\n";
let claimed_sha = "deadbeef".to_string();
let (sha, _, _, _) = gc
.compress(
&key(&[b"label"]),
&[text.as_slice()],
text.len(),
Some(claimed_sha.clone()),
None,
None,
)
.unwrap();
assert_eq!(sha, claimed_sha);
}
#[test]
fn rabin_compressor_content_addressed_key_substitution() {
let mut gc = RabinGroupCompressor::new(None);
let text = b"content-addressed body\n";
let key = Key::ContentAddressed(vec![b"prefix".to_vec()]);
let (sha, _, _, _) = gc
.compress(&key, &[text.as_slice()], text.len(), None, None, None)
.unwrap();
let expected_sha = crate::osutils::sha::sha_chunks(&[text.as_slice()]);
assert_eq!(sha, expected_sha);
let stored_key = vec![
b"prefix".to_vec(),
format!("sha1:{}", expected_sha).into_bytes(),
];
assert!(gc.labels_deltas().contains_key(&stored_key));
let (data, _) = gc.extract(&stored_key).unwrap();
assert_eq!(data, vec![text.to_vec()]);
}
#[test]
fn rabin_compressor_extract_after_intermediate_delta() {
let mut gc = RabinGroupCompressor::new(None);
let base = b"common prefix that is long enough to be worth indexing\nshared\n";
let derived =
b"common prefix that is long enough to be worth indexing\nshared\nplus more\n";
gc.compress(
&key(&[b"base"]),
&[base.as_slice()],
base.len(),
None,
None,
None,
)
.unwrap();
gc.compress(
&key(&[b"derived"]),
&[derived.as_slice()],
derived.len(),
None,
None,
None,
)
.unwrap();
let (data, _) = gc.extract(&vec![b"base".to_vec()]).unwrap();
assert_eq!(data, vec![base.to_vec()]);
let (data, _) = gc.extract(&vec![b"derived".to_vec()]).unwrap();
assert_eq!(data, vec![derived.to_vec()]);
}
#[test]
fn rabin_compressor_extract_prefix_cache_orders() {
let mut gc = RabinGroupCompressor::new(None);
let base = b"common prefix that is long enough to be worth indexing\nshared\n";
let d1 = b"common prefix that is long enough to be worth indexing\nshared\none\n";
let d2 = b"common prefix that is long enough to be worth indexing\nshared\none\ntwo\n";
for (k, text) in [
(b"base".as_slice(), base.as_slice()),
(b"d1".as_slice(), d1.as_slice()),
(b"d2".as_slice(), d2.as_slice()),
] {
gc.compress(&key(&[k]), &[text], text.len(), None, None, None)
.unwrap();
}
assert_eq!(
gc.extract(&vec![b"d2".to_vec()]).unwrap().0,
vec![d2.to_vec()]
);
assert_eq!(
gc.extract(&vec![b"d1".to_vec()]).unwrap().0,
vec![d1.to_vec()]
);
assert_eq!(
gc.extract(&vec![b"base".to_vec()]).unwrap().0,
vec![base.to_vec()]
);
assert_eq!(
gc.extract(&vec![b"d2".to_vec()]).unwrap().0,
vec![d2.to_vec()]
);
}
#[test]
fn rabin_compressor_flush_without_last_drops_final_record() {
let mut gc = RabinGroupCompressor::new(None);
gc.compress(
&key(&[b"a"]),
&[b"first record\n".as_slice()],
13,
None,
None,
None,
)
.unwrap();
let endpoint_after_first = gc.endpoint();
gc.compress(
&key(&[b"b"]),
&[b"second record\n".as_slice()],
14,
None,
None,
None,
)
.unwrap();
let (chunks, endpoint) = gc.flush_without_last();
assert_eq!(endpoint, endpoint_after_first);
let total: usize = chunks.iter().map(|c| c.len()).sum();
assert_eq!(total, endpoint_after_first);
}
#[test]
fn rabin_compressor_input_chunks_can_be_split() {
let mut single = RabinGroupCompressor::new(None);
let one_shot = b"hello world\nthis is a single slice\n";
single
.compress(
&key(&[b"k"]),
&[one_shot.as_slice()],
one_shot.len(),
None,
None,
None,
)
.unwrap();
let mut multi = RabinGroupCompressor::new(None);
let parts: &[&[u8]] = &[b"hello world\n", b"this is a single slice\n"];
let total_len: usize = parts.iter().map(|p| p.len()).sum();
multi
.compress(&key(&[b"k"]), parts, total_len, None, None, None)
.unwrap();
assert_eq!(single.chunks().concat(), multi.chunks().concat());
assert_eq!(single.endpoint(), multi.endpoint());
let stored_key = vec![b"k".to_vec()];
let (single_data, _) = single.extract(&stored_key).unwrap();
let (multi_data, _) = multi.extract(&stored_key).unwrap();
assert_eq!(single_data.concat(), multi_data.concat());
}
#[test]
fn rabin_compressor_ratio_zero_for_empty_compressor() {
let gc = RabinGroupCompressor::new(None);
assert_eq!(gc.ratio(), 0.0);
}
#[test]
fn rabin_compressor_ratio_above_one_after_compression() {
let mut gc = RabinGroupCompressor::new(None);
let text = b"the same long line repeated for compression\n".repeat(8);
gc.compress(
&key(&[b"a"]),
&[text.as_slice()],
text.len(),
None,
None,
None,
)
.unwrap();
gc.compress(
&key(&[b"b"]),
&[text.as_slice()],
text.len(),
None,
None,
None,
)
.unwrap();
assert!(gc.ratio() > 1.0);
}
#[test]
fn traditional_compressor_round_trips_fulltext() {
let mut gc = TraditionalGroupCompressor::new();
let text = b"hello world\nthis is a line-based fulltext\n";
let (sha, start, end, kind) = gc
.compress(
&key(&[b"label"]),
&[text.as_slice()],
text.len(),
None,
None,
None,
)
.unwrap();
assert_eq!(kind, RecordKind::Fulltext);
assert!(end > start);
assert!(!sha.is_empty());
let stored_key: Vec<Vec<u8>> = vec![b"label".to_vec()];
let (data, data_sha) = gc.extract(&stored_key).unwrap();
assert_eq!(data.concat(), text.to_vec());
assert_eq!(data_sha, sha);
}
#[test]
fn traditional_compressor_delta_copies_from_two_parents() {
let mut gc = TraditionalGroupCompressor::new();
let t1 = b"strange\ncommon very very long line\nwith some extra text\n";
gc.compress(
&key(&[b"label"]),
&[t1.as_slice()],
t1.len(),
None,
None,
None,
)
.unwrap();
let t2 = b"different\nmoredifferent\nand then some more\n";
gc.compress(
&key(&[b"newlabel"]),
&[t2.as_slice()],
t2.len(),
None,
None,
None,
)
.unwrap();
let chunks_before: usize = gc.chunks().iter().map(|c| c.len()).sum();
let t3 = b"new\ncommon very very long line\nwith some extra text\n\
different\nmoredifferent\nand then some more\n";
let (sha, _start, end, kind) = gc
.compress(
&key(&[b"label3"]),
&[t3.as_slice()],
t3.len(),
None,
None,
None,
)
.unwrap();
assert_eq!(kind, RecordKind::Delta);
assert_eq!(sha, crate::osutils::sha::sha_string(t3));
let all: Vec<u8> = gc.chunks().concat();
let new_delta = &all[chunks_before..];
let expected: &[u8] = b"d\x0c\x5f\x04new\n\x91\x0a\x30\x91\x3c\x2b";
assert_eq!(new_delta, expected);
assert_eq!(end - chunks_before, expected.len());
let (data, _) = gc.extract(&vec![b"label3".to_vec()]).unwrap();
assert_eq!(data.concat(), t3.to_vec());
}
#[test]
fn traditional_compressor_round_trips_delta() {
let mut gc = TraditionalGroupCompressor::new();
let base = b"shared line one\nshared line two\nshared line three\nshared line four\n";
let derived =
b"shared line one\nshared line two\nshared line three\nshared line four\nplus extra\n";
gc.compress(
&key(&[b"base"]),
&[base.as_slice()],
base.len(),
None,
None,
None,
)
.unwrap();
let (_sha, _start, _end, kind) = gc
.compress(
&key(&[b"derived"]),
&[derived.as_slice()],
derived.len(),
None,
None,
None,
)
.unwrap();
assert_eq!(kind, RecordKind::Delta);
let (data, _) = gc.extract(&vec![b"derived".to_vec()]).unwrap();
assert_eq!(data.concat(), derived.to_vec());
let (base_data, _) = gc.extract(&vec![b"base".to_vec()]).unwrap();
assert_eq!(base_data.concat(), base.to_vec());
}
#[test]
fn traditional_compressor_empty_input_returns_null_sha() {
let mut gc = TraditionalGroupCompressor::new();
let (sha, start, end, kind) = gc
.compress(&key(&[b"empty"]), &[], 0, None, None, None)
.unwrap();
assert_eq!(start, 0);
assert_eq!(end, 0);
assert_eq!(kind, RecordKind::Fulltext);
assert_eq!(sha.as_bytes(), crate::groupcompress::NULL_SHA1.as_slice());
}
#[test]
fn traditional_compressor_nostore_sha_match_raises_existing_content() {
let mut gc = TraditionalGroupCompressor::new();
let text = b"some line-delta content\n";
let actual_sha = crate::osutils::sha::sha_chunks(&[text.as_slice()]);
let err = gc
.compress(
&key(&[b"label"]),
&[text.as_slice()],
text.len(),
None,
Some(actual_sha),
None,
)
.unwrap_err();
assert!(matches!(err, Error::ExistingContent(_)));
assert_eq!(gc.endpoint(), 0);
}
#[test]
fn traditional_compressor_content_addressed_key_substitution() {
let mut gc = TraditionalGroupCompressor::new();
let text = b"content-addressed body\n";
let key = Key::ContentAddressed(vec![b"prefix".to_vec()]);
let (sha, _, _, _) = gc
.compress(&key, &[text.as_slice()], text.len(), None, None, None)
.unwrap();
let expected_sha = crate::osutils::sha::sha_chunks(&[text.as_slice()]);
assert_eq!(sha, expected_sha);
let stored_key = vec![
b"prefix".to_vec(),
format!("sha1:{}", expected_sha).into_bytes(),
];
let (data, _) = gc.extract(&stored_key).unwrap();
assert_eq!(data.concat(), text.to_vec());
}
#[test]
fn traditional_compressor_ratio_zero_for_empty_compressor() {
let gc = TraditionalGroupCompressor::new();
assert_eq!(gc.ratio(), 0.0);
}
#[test]
fn traditional_compressor_ratio_above_one_after_compression() {
let mut gc = TraditionalGroupCompressor::new();
let text = b"the same long line repeated for compression\n".repeat(8);
gc.compress(
&key(&[b"a"]),
&[text.as_slice()],
text.len(),
None,
None,
None,
)
.unwrap();
gc.compress(
&key(&[b"b"]),
&[text.as_slice()],
text.len(),
None,
None,
None,
)
.unwrap();
assert!(gc.ratio() > 1.0);
}
}