use crate::groupcompress::block::GroupCompressBlock;
use crate::knit::FileRef;
pub type GcKey = crate::versionedfile::Key;
pub const BATCH_SIZE: u64 = 1 << 16;
pub const DEFAULT_MAX_BYTES_TO_INDEX: usize = 1024 * 1024;
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub struct ReadMemo<F: FileRef = String> {
pub index: F,
pub start: u64,
pub stop: u64,
}
impl<F: FileRef> ReadMemo<F> {
pub fn new(index: F, start: u64, stop: u64) -> Self {
ReadMemo { index, start, stop }
}
pub fn byte_length(&self) -> u64 {
self.stop.saturating_sub(self.start)
}
}
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub struct IndexMemo<F: FileRef = String> {
pub read_memo: ReadMemo<F>,
pub entry_start: u64,
pub entry_end: u64,
}
impl<F: FileRef> IndexMemo<F> {
pub fn new(read_memo: ReadMemo<F>, entry_start: u64, entry_end: u64) -> Self {
IndexMemo {
read_memo,
entry_start,
entry_end,
}
}
}
pub struct FetchedBlock<F: FileRef = String> {
pub read_memo: ReadMemo<F>,
pub block: GroupCompressBlock,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct GcBuildDetails<F: FileRef = String> {
pub index_memo: IndexMemo<F>,
pub parents: Option<Vec<GcKey>>,
}
pub trait GcIndex {
type F: FileRef;
fn get_build_details(
&self,
keys: &[GcKey],
) -> Result<std::collections::HashMap<GcKey, GcBuildDetails<Self::F>>, crate::knit::KnitError>;
fn get_parent_map(
&self,
keys: &[GcKey],
) -> Result<std::collections::HashMap<GcKey, Vec<GcKey>>, crate::knit::KnitError>;
fn keys(&self) -> Result<Vec<GcKey>, crate::knit::KnitError>;
fn has_graph(&self) -> bool;
fn check_write_ok(&self) -> Result<(), crate::knit::KnitError>;
fn add_records(
&self,
records: &[(GcKey, IndexMemo<Self::F>, Option<Vec<GcKey>>)],
random_id: bool,
) -> Result<(), crate::knit::KnitError>;
}
pub trait GcAccess {
type F: FileRef;
fn get_raw_records(
&self,
memos: &[ReadMemo<Self::F>],
) -> Result<Vec<Vec<u8>>, crate::knit::KnitError>;
fn add_raw_record(
&self,
size: usize,
chunks: Vec<Vec<u8>>,
) -> Result<ReadMemo<Self::F>, crate::knit::KnitError>;
fn reload_or_raise(&self, err: crate::knit::KnitError) -> Result<(), crate::knit::KnitError> {
Err(err)
}
}
pub type SharedBlock = std::sync::Arc<std::sync::Mutex<GroupCompressBlock>>;
pub trait BlockCache<F: FileRef>: Send + Sync {
fn get(&self, memo: &ReadMemo<F>) -> Option<SharedBlock>;
fn insert(&self, memo: ReadMemo<F>, block: SharedBlock);
fn contains(&self, memo: &ReadMemo<F>) -> bool {
self.get(memo).is_some()
}
fn clear(&self);
fn len(&self) -> usize;
fn is_empty(&self) -> bool {
self.len() == 0
}
}
pub struct MapBlockCache<F: FileRef> {
inner: std::sync::Mutex<std::collections::HashMap<ReadMemo<F>, SharedBlock>>,
}
impl<F: FileRef> Default for MapBlockCache<F> {
fn default() -> Self {
MapBlockCache {
inner: std::sync::Mutex::new(std::collections::HashMap::new()),
}
}
}
impl<F: FileRef> BlockCache<F> for MapBlockCache<F> {
fn get(&self, memo: &ReadMemo<F>) -> Option<SharedBlock> {
self.inner.lock().unwrap().get(memo).cloned()
}
fn insert(&self, memo: ReadMemo<F>, block: SharedBlock) {
self.inner.lock().unwrap().insert(memo, block);
}
fn clear(&self) {
self.inner.lock().unwrap().clear();
}
fn len(&self) -> usize {
self.inner.lock().unwrap().len()
}
}
pub fn memos_to_fetch<F: FileRef>(
read_memos: &[ReadMemo<F>],
is_cached: impl Fn(&ReadMemo<F>) -> bool,
) -> Vec<ReadMemo<F>> {
let mut out: Vec<ReadMemo<F>> = Vec::new();
let mut seen: std::collections::HashSet<ReadMemo<F>> = std::collections::HashSet::new();
for memo in read_memos {
if is_cached(memo) {
continue;
}
if seen.insert(memo.clone()) {
out.push(memo.clone());
}
}
out
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Source {
Local,
Fallback(usize),
}
fn group_by_source(
keys: impl IntoIterator<Item = GcKey>,
source_of: impl Fn(&GcKey) -> Source,
) -> Vec<(Source, Vec<GcKey>)> {
let mut runs: Vec<(Source, Vec<GcKey>)> = Vec::new();
for key in keys {
let source = source_of(&key);
match runs.last_mut() {
Some((s, run)) if *s == source => run.push(key),
_ => runs.push((source, vec![key])),
}
}
runs
}
pub fn ordered_source_keys(
ordering: &str,
parent_map: &[(GcKey, Vec<GcKey>)],
key_to_source: &std::collections::HashMap<GcKey, Source>,
) -> Vec<(Source, Vec<GcKey>)> {
let raw: Vec<(Vec<Vec<u8>>, Vec<Vec<Vec<u8>>>)> = parent_map
.iter()
.map(|(k, ps)| {
(
k.segments().to_vec(),
ps.iter().map(|p| p.segments().to_vec()).collect(),
)
})
.collect();
let present: Vec<Vec<Vec<u8>>> = if ordering == "topological" {
let mut sorter = vcs_graph::tsort::TopoSorter::new(raw.into_iter());
sorter
.sorted()
.expect("groupcompress parent_map should not contain cycles")
} else {
crate::groupcompress::sort::sort_gc_optimal(raw)
};
let keys = present.into_iter().map(GcKey::fixed);
group_by_source(keys, |k| {
key_to_source.get(k).copied().unwrap_or(Source::Local)
})
}
pub fn as_requested_source_keys(
orig_keys: &[GcKey],
locations: &std::collections::HashSet<GcKey>,
unadded: &std::collections::HashSet<GcKey>,
key_to_source: &std::collections::HashMap<GcKey, Source>,
) -> Vec<(Source, Vec<GcKey>)> {
let present: Vec<GcKey> = orig_keys
.iter()
.filter(|k| {
locations.contains(*k) || unadded.contains(*k) || key_to_source.contains_key(*k)
})
.cloned()
.collect();
group_by_source(present, |k| {
if locations.contains(k) || unadded.contains(k) {
Source::Local
} else {
key_to_source.get(k).copied().unwrap_or(Source::Local)
}
})
}
#[derive(Debug, Default)]
pub struct BatchAccumulator<F: FileRef = String> {
keys: Vec<GcKey>,
batch_memos: Vec<ReadMemo<F>>,
memos_to_get: Vec<ReadMemo<F>>,
total_bytes: u64,
}
impl<F: FileRef> BatchAccumulator<F> {
pub fn new() -> Self {
BatchAccumulator {
keys: Vec::new(),
batch_memos: Vec::new(),
memos_to_get: Vec::new(),
total_bytes: 0,
}
}
pub fn add_key(
&mut self,
key: GcKey,
read_memo: ReadMemo<F>,
is_cached: impl Fn(&ReadMemo<F>) -> bool,
) -> u64 {
self.keys.push(key);
if self.batch_memos.contains(&read_memo) {
return self.total_bytes;
}
if !is_cached(&read_memo) {
self.total_bytes += read_memo.stop;
self.memos_to_get.push(read_memo.clone());
}
self.batch_memos.push(read_memo);
self.total_bytes
}
pub fn keys(&self) -> &[GcKey] {
&self.keys
}
pub fn memos_to_get(&self) -> &[ReadMemo<F>] {
&self.memos_to_get
}
pub fn total_bytes(&self) -> u64 {
self.total_bytes
}
pub fn reset(&mut self) {
self.keys.clear();
self.batch_memos.clear();
self.memos_to_get.clear();
self.total_bytes = 0;
}
}
pub fn io_ordered_source_keys<F: FileRef>(
located_keys: &[GcKey],
locations: &std::collections::HashMap<GcKey, IndexMemo<F>>,
unadded: &[GcKey],
fallback_runs: Vec<(Source, Vec<GcKey>)>,
) -> Vec<(Source, Vec<GcKey>)> {
let mut local: Vec<GcKey> = unadded.to_vec();
let mut located: Vec<GcKey> = located_keys.to_vec();
located.sort_by(|a, b| {
locations[a]
.read_memo
.index
.cmp(&locations[b].read_memo.index)
});
local.extend(located);
let mut runs = vec![(Source::Local, local)];
runs.extend(fallback_runs);
runs
}
pub struct GroupCompressVersionedFiles<I, A, C = MapBlockCache<<I as GcIndex>::F>>
where
I: GcIndex,
A: GcAccess<F = I::F>,
C: BlockCache<I::F>,
{
index: I,
access: A,
delta: bool,
block_cache: C,
fallbacks: Vec<Box<dyn crate::versionedfile::VersionedFiles>>,
}
impl<I, A> GroupCompressVersionedFiles<I, A, MapBlockCache<I::F>>
where
I: GcIndex,
A: GcAccess<F = I::F>,
{
pub fn new(index: I, access: A, delta: bool) -> Self {
GroupCompressVersionedFiles::with_cache(index, access, delta, MapBlockCache::default())
}
}
impl<I, A, C> GroupCompressVersionedFiles<I, A, C>
where
I: GcIndex,
A: GcAccess<F = I::F>,
C: BlockCache<I::F>,
{
pub fn with_cache(index: I, access: A, delta: bool, block_cache: C) -> Self {
GroupCompressVersionedFiles {
index,
access,
delta,
block_cache,
fallbacks: Vec::new(),
}
}
pub fn block_cache(&self) -> &C {
&self.block_cache
}
pub fn add_fallback_versioned_files(
&mut self,
fallback: Box<dyn crate::versionedfile::VersionedFiles>,
) {
self.fallbacks.push(fallback);
}
pub fn index(&self) -> &I {
&self.index
}
pub fn access(&self) -> &A {
&self.access
}
pub fn delta(&self) -> bool {
self.delta
}
#[allow(clippy::type_complexity)]
pub fn get_blocks(
&self,
read_memos: &[ReadMemo<I::F>],
) -> Result<Vec<(ReadMemo<I::F>, SharedBlock)>, crate::knit::KnitError> {
let to_fetch = memos_to_fetch(read_memos, |m| self.block_cache.contains(m));
let raw = self.access.get_raw_records(&to_fetch)?;
if raw.len() != to_fetch.len() {
return Err(crate::knit::KnitError::Corrupt(
"get_raw_records returned the wrong number of records".to_string(),
));
}
for (memo, zdata) in to_fetch.iter().zip(raw) {
let block = GroupCompressBlock::from_bytes(&zdata[..])
.map_err(|e| crate::knit::KnitError::Corrupt(e.to_string()))?;
self.block_cache.insert(
memo.clone(),
std::sync::Arc::new(std::sync::Mutex::new(block)),
);
}
let mut out = Vec::with_capacity(read_memos.len());
for memo in read_memos {
let block = self.block_cache.get(memo).ok_or_else(|| {
crate::knit::KnitError::Corrupt("block missing after fetch".to_string())
})?;
out.push((memo.clone(), block));
}
Ok(out)
}
pub fn get_parent_map(
&self,
keys: &[GcKey],
) -> Result<std::collections::HashMap<GcKey, Vec<GcKey>>, crate::knit::KnitError> {
let mut result = self.index.get_parent_map(keys)?;
let mut missing: Vec<GcKey> = keys
.iter()
.filter(|k| !result.contains_key(*k))
.cloned()
.collect();
for fb in &self.fallbacks {
if missing.is_empty() {
break;
}
let found = fb.get_parent_map(&missing)?;
missing.retain(|k| !found.contains_key(k));
result.extend(found);
}
Ok(result)
}
pub fn keys(&self) -> Result<Vec<GcKey>, crate::knit::KnitError> {
let mut seen: std::collections::HashSet<GcKey> = self.index.keys()?.into_iter().collect();
for fb in &self.fallbacks {
for k in fb.keys()? {
seen.insert(k);
}
}
Ok(seen.into_iter().collect())
}
pub fn get_record_stream(
&self,
keys: &[GcKey],
ordering: &str,
) -> Result<Vec<Box<dyn crate::versionedfile::ContentFactory>>, crate::knit::KnitError> {
let locations = self.index.get_build_details(keys)?;
let mut out: Vec<Box<dyn crate::versionedfile::ContentFactory>> = Vec::new();
let mut nonlocal: Vec<GcKey> = keys
.iter()
.filter(|k| !locations.contains_key(*k))
.cloned()
.collect();
for fb in &self.fallbacks {
if nonlocal.is_empty() {
break;
}
let fb_records = fb.get_record_stream(&nonlocal, ordering, true)?;
let mut still_missing: Vec<GcKey> = Vec::new();
let mut found: std::collections::HashSet<GcKey> = std::collections::HashSet::new();
for rec in fb_records {
let rec = rec?;
if rec.storage_kind() == "absent" {
continue;
}
found.insert(rec.key());
out.push(rec);
}
for k in nonlocal {
if !found.contains(&k) {
still_missing.push(k);
}
}
nonlocal = still_missing;
}
for key in nonlocal {
out.push(Box::new(crate::versionedfile::AbsentContentFactory::new(
key,
)));
}
let located: Vec<GcKey> = keys
.iter()
.filter(|k| locations.contains_key(*k))
.cloned()
.collect();
let ordered: Vec<GcKey> = match ordering {
"topological" | "groupcompress" => {
let parent_map: Vec<(GcKey, Vec<GcKey>)> = located
.iter()
.map(|k| (k.clone(), locations[k].parents.clone().unwrap_or_default()))
.collect();
let empty = std::collections::HashMap::new();
ordered_source_keys(ordering, &parent_map, &empty)
.into_iter()
.flat_map(|(_, ks)| ks)
.collect()
}
"as-requested" => located,
_ => {
let mut io = located;
io.sort_by(|a, b| {
let ka = &locations[a].index_memo;
let kb = &locations[b].index_memo;
(
&ka.read_memo.index,
ka.read_memo.start,
ka.read_memo.stop,
ka.entry_start,
ka.entry_end,
)
.cmp(&(
&kb.read_memo.index,
kb.read_memo.start,
kb.read_memo.stop,
kb.entry_start,
kb.entry_end,
))
});
io
}
};
let read_memos: Vec<ReadMemo<I::F>> = ordered
.iter()
.map(|k| locations[k].index_memo.read_memo.clone())
.collect();
let blocks = self.get_blocks(&read_memos)?;
let block_of: std::collections::HashMap<ReadMemo<I::F>, SharedBlock> =
blocks.into_iter().collect();
for key in &ordered {
let memo = &locations[key].index_memo;
let block = block_of.get(&memo.read_memo).ok_or_else(|| {
crate::knit::KnitError::Corrupt("block missing for located key".to_string())
})?;
let chunks = block
.lock()
.unwrap()
.extract(memo.entry_start as usize, memo.entry_end as usize)
.map_err(|e| crate::knit::KnitError::Corrupt(format!("{:?}", e)))?;
out.push(Box::new(crate::versionedfile::ChunkedContentFactory::new(
None,
key.clone(),
locations[key].parents.clone(),
chunks,
)));
}
Ok(out)
}
pub fn insert_record_stream(
&self,
stream: impl IntoIterator<Item = Box<dyn crate::versionedfile::ContentFactory>>,
random_id: bool,
) -> Result<Vec<(Vec<u8>, usize)>, crate::knit::KnitError> {
use crate::groupcompress::compressor::{GroupCompressor, RabinGroupCompressor};
self.index.check_write_ok()?;
let mut results: Vec<(Vec<u8>, usize)> = Vec::new();
let mut compressor = RabinGroupCompressor::new(None);
let mut pending: Vec<(GcKey, usize, usize, Option<Vec<GcKey>>)> = Vec::new();
let mut inserted: std::collections::HashSet<GcKey> = std::collections::HashSet::new();
let mut last_prefix: Option<Vec<u8>> = None;
let mut max_fulltext_len: usize = 0;
let mut max_fulltext_prefix: Option<Vec<u8>> = None;
for record in stream {
let key = record.key();
if record.storage_kind() == "absent" {
return Err(crate::knit::KnitError::RevisionNotPresent(
key.segments().to_vec(),
));
}
if random_id && !inserted.insert(key.clone()) {
continue;
}
let chunks: Vec<Vec<u8>> = record.to_chunks().map(|c| c.into_owned()).collect();
let chunks_len: usize = record
.size()
.unwrap_or_else(|| chunks.iter().map(|c| c.len()).sum());
let chunk_refs: Vec<&[u8]> = chunks.iter().map(|c| c.as_slice()).collect();
let prefix: Option<Vec<u8>> = if key.segments().len() > 1 {
Some(key.segments()[0].clone())
} else {
None
};
let soft = prefix.is_some() && prefix == last_prefix;
if max_fulltext_len < chunks_len {
max_fulltext_len = chunks_len;
max_fulltext_prefix = prefix.clone();
}
let (mut sha1, mut start_point, mut end_point) = compress_record(
&mut compressor,
&key,
&chunk_refs,
chunks_len,
record.sha1(),
soft,
)?;
let same_prefix = prefix == max_fulltext_prefix;
let start_new_block = if same_prefix && end_point < 2 * max_fulltext_len {
false
} else if end_point > 4 * 1024 * 1024 {
true
} else {
prefix.is_some() && prefix != last_prefix && end_point > 2 * 1024 * 1024
};
last_prefix = prefix;
if start_new_block {
let (content_chunks, content_len) = compressor.flush_without_last();
self.flush_block(content_chunks, content_len, &mut pending, random_id)?;
compressor = RabinGroupCompressor::new(None);
max_fulltext_len = chunks_len;
let recompressed = compress_record(
&mut compressor,
&key,
&chunk_refs,
chunks_len,
record.sha1(),
false,
)?;
sha1 = recompressed.0;
start_point = recompressed.1;
end_point = recompressed.2;
}
let stored_key = if key.version_id().is_empty() {
GcKey::from_prefix_and_suffix(key.prefix(), {
let mut seg = b"sha1:".to_vec();
seg.extend_from_slice(&sha1);
seg
})
} else {
key.clone()
};
results.push((sha1, chunks_len));
pending.push((stored_key, start_point, end_point, record.parents()));
}
if !pending.is_empty() {
let (content_chunks, content_len) = compressor.flush();
self.flush_block(content_chunks, content_len, &mut pending, random_id)?;
}
Ok(results)
}
fn flush_block(
&self,
content_chunks: Vec<Vec<u8>>,
content_len: usize,
pending: &mut Vec<(GcKey, usize, usize, Option<Vec<GcKey>>)>,
random_id: bool,
) -> Result<(), crate::knit::KnitError> {
let mut block = GroupCompressBlock::new();
block.set_chunked_content(&content_chunks, content_len);
let on_disk = block.to_bytes();
let size = on_disk.len();
let read_memo = self.access.add_raw_record(size, vec![on_disk])?;
let records: Vec<(GcKey, IndexMemo<I::F>, Option<Vec<GcKey>>)> = pending
.drain(..)
.map(|(key, start, end, parents)| {
(
key,
IndexMemo::new(read_memo.clone(), start as u64, end as u64),
parents,
)
})
.collect();
self.index.add_records(&records, random_id)?;
Ok(())
}
pub fn add_lines(
&self,
key: GcKey,
parents: Option<Vec<GcKey>>,
lines: Vec<Vec<u8>>,
) -> Result<(Vec<u8>, usize), crate::knit::KnitError> {
let version_id = key.version_id();
if version_id
.iter()
.any(|b| matches!(b, b' ' | b'\t' | b'\n' | b'\r' | 0x0b | 0x0c))
{
return Err(crate::knit::KnitError::Corrupt(format!(
"invalid revision id: {:?}",
version_id
)));
}
let sha1 = crate::weave::sha_strings(&lines);
let factory: Box<dyn crate::versionedfile::ContentFactory> = Box::new(
crate::versionedfile::ChunkedContentFactory::new(Some(sha1), key, parents, lines),
);
let mut inserted = self.insert_record_stream(std::iter::once(factory), false)?;
inserted
.pop()
.ok_or_else(|| crate::knit::KnitError::Corrupt("add_lines inserted nothing".into()))
}
pub fn get_sha1s(
&self,
keys: &[GcKey],
) -> Result<std::collections::HashMap<GcKey, Vec<u8>>, crate::knit::KnitError> {
let mut out = std::collections::HashMap::new();
for record in self.get_record_stream(keys, "unordered")? {
if record.storage_kind() == "absent" {
continue;
}
let digest = match record.sha1() {
Some(s) => s,
None => {
let chunks: Vec<Vec<u8>> = record.to_chunks().map(|c| c.into_owned()).collect();
crate::weave::sha_strings(&chunks)
}
};
out.insert(record.key(), digest);
}
Ok(out)
}
pub fn get_missing_compression_parent_keys(&self) -> Vec<GcKey> {
Vec::new()
}
pub fn clear_cache(&self) {
self.block_cache.clear();
}
pub fn iter_lines_added_or_present_in_keys(
&self,
keys: &[GcKey],
) -> Result<
impl Iterator<Item = Result<(Vec<u8>, GcKey), crate::knit::KnitError>>,
crate::knit::KnitError,
> {
let records = self.get_record_stream(keys, "unordered")?;
Ok(records.into_iter().flat_map(|record| {
if record.storage_kind() == "absent" {
let err =
crate::knit::KnitError::RevisionNotPresent(record.key().segments().to_vec());
return vec![Err(err)].into_iter();
}
let key = record.key();
let chunks: Vec<Vec<u8>> = record.to_chunks().map(|c| c.into_owned()).collect();
crate::osutils::chunks_to_lines(chunks.into_iter().map(Ok::<_, std::io::Error>))
.map(|line| {
line.map(|l| (l.into_owned(), key.clone()))
.map_err(|e| crate::knit::KnitError::Corrupt(e.to_string()))
})
.collect::<Vec<_>>()
.into_iter()
}))
}
pub fn check(&self) -> Result<(), crate::knit::KnitError> {
let all_keys = self.keys()?;
for record in self.get_record_stream(&all_keys, "unordered")? {
if record.storage_kind() == "absent" {
return Err(crate::knit::KnitError::RevisionNotPresent(
record.key().segments().to_vec(),
));
}
let _ = record.to_fulltext();
}
Ok(())
}
}
impl<I, A, C> crate::versionedfile::VersionedFiles for GroupCompressVersionedFiles<I, A, C>
where
I: GcIndex + Send + Sync,
A: GcAccess<F = I::F> + Send + Sync,
C: BlockCache<I::F> + Send + Sync,
{
fn get_parent_map(
&self,
keys: &[GcKey],
) -> Result<std::collections::HashMap<GcKey, Vec<GcKey>>, crate::knit::KnitError> {
GroupCompressVersionedFiles::get_parent_map(self, keys)
}
fn get_record_stream(
&self,
keys: &[GcKey],
ordering: &str,
_include_delta_closure: bool,
) -> Result<
Box<
dyn Iterator<
Item = Result<
Box<dyn crate::versionedfile::ContentFactory>,
crate::knit::KnitError,
>,
>,
>,
crate::knit::KnitError,
> {
let records = GroupCompressVersionedFiles::get_record_stream(self, keys, ordering)?;
Ok(Box::new(records.into_iter().map(Ok)))
}
fn get_sha1s(
&self,
keys: &[GcKey],
) -> Result<std::collections::HashMap<GcKey, Vec<u8>>, crate::knit::KnitError> {
GroupCompressVersionedFiles::get_sha1s(self, keys)
}
fn keys(&self) -> Result<Vec<GcKey>, crate::knit::KnitError> {
GroupCompressVersionedFiles::keys(self)
}
fn add_lines(
&self,
key: &GcKey,
parents: Option<&[GcKey]>,
lines: &[Vec<u8>],
) -> Result<(Vec<u8>, usize), crate::knit::KnitError> {
GroupCompressVersionedFiles::add_lines(
self,
key.clone(),
parents.map(|p| p.to_vec()),
lines.to_vec(),
)
}
fn insert_record_stream(
&self,
stream: Box<dyn Iterator<Item = Box<dyn crate::versionedfile::ContentFactory>>>,
) -> Result<(), crate::knit::KnitError> {
GroupCompressVersionedFiles::insert_record_stream(self, stream, false)?;
Ok(())
}
fn iter_lines_added_or_present_in_keys(
&self,
keys: &[GcKey],
) -> Result<Vec<(Vec<u8>, GcKey)>, crate::knit::KnitError> {
GroupCompressVersionedFiles::iter_lines_added_or_present_in_keys(self, keys)?
.collect::<Result<Vec<_>, _>>()
}
fn annotate(&self, _key: &GcKey) -> Result<Vec<(GcKey, Vec<u8>)>, crate::knit::KnitError> {
Err(crate::knit::KnitError::NotImplemented(
"groupcompress annotate",
))
}
fn get_missing_compression_parent_keys(&self) -> Result<Vec<GcKey>, crate::knit::KnitError> {
Ok(GroupCompressVersionedFiles::get_missing_compression_parent_keys(self))
}
fn clear_cache(&self) {
GroupCompressVersionedFiles::clear_cache(self)
}
fn check(&self) -> Result<(), crate::knit::KnitError> {
GroupCompressVersionedFiles::check(self)
}
}
fn compress_record(
compressor: &mut crate::groupcompress::compressor::RabinGroupCompressor,
key: &GcKey,
chunks: &[&[u8]],
length: usize,
expected_sha: Option<Vec<u8>>,
soft: bool,
) -> Result<(Vec<u8>, usize, usize), crate::knit::KnitError> {
use crate::groupcompress::compressor::GroupCompressor;
let expected = expected_sha
.map(|s| String::from_utf8(s))
.transpose()
.map_err(|e| crate::knit::KnitError::Corrupt(e.to_string()))?;
match compressor.compress(key, chunks, length, expected, None, Some(soft)) {
Ok((sha, start, end, _kind)) => Ok((sha.into_bytes(), start, end)),
Err(crate::versionedfile::Error::ExistingContent(_)) => {
Err(crate::knit::KnitError::ExistingContent(Vec::new()))
}
Err(e) => Err(crate::knit::KnitError::Corrupt(e.to_string())),
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn read_memo_byte_length_is_stop_minus_start() {
let memo = ReadMemo::new("idx".to_string(), 100, 350);
assert_eq!(memo.byte_length(), 250);
}
#[test]
fn read_memo_byte_length_saturates_when_stop_below_start() {
let memo = ReadMemo::new("idx".to_string(), 500, 100);
assert_eq!(memo.byte_length(), 0);
}
#[test]
fn read_memos_compare_by_all_three_fields() {
let base = ReadMemo::new("a".to_string(), 0, 10);
assert_eq!(base, ReadMemo::new("a".to_string(), 0, 10));
assert_ne!(base, ReadMemo::new("b".to_string(), 0, 10));
assert_ne!(base, ReadMemo::new("a".to_string(), 1, 10));
assert_ne!(base, ReadMemo::new("a".to_string(), 0, 11));
}
#[test]
fn index_memo_carries_read_memo_and_entry_range() {
let rm = ReadMemo::new("idx".to_string(), 0, 1000);
let im = IndexMemo::new(rm.clone(), 40, 120);
assert_eq!(im.read_memo, rm);
assert_eq!(im.entry_start, 40);
assert_eq!(im.entry_end, 120);
}
fn memo(idx: &str, start: u64) -> ReadMemo<String> {
ReadMemo::new(idx.to_string(), start, start + 10)
}
#[test]
fn memos_to_fetch_skips_cached_and_preserves_order() {
let req = vec![memo("a", 0), memo("b", 0), memo("c", 0)];
let cached = [memo("b", 0)];
let out = memos_to_fetch(&req, |m| cached.contains(m));
assert_eq!(out, vec![memo("a", 0), memo("c", 0)]);
}
#[test]
fn memos_to_fetch_dedups_repeated_memos() {
let req = vec![memo("a", 0), memo("b", 0), memo("a", 0), memo("c", 0)];
let out = memos_to_fetch(&req, |_| false);
assert_eq!(out, vec![memo("a", 0), memo("b", 0), memo("c", 0)]);
}
#[test]
fn memos_to_fetch_empty_when_all_cached() {
let req = vec![memo("a", 0), memo("b", 0)];
let out = memos_to_fetch(&req, |_| true);
assert!(out.is_empty());
}
fn gckey(id: &[u8]) -> GcKey {
GcKey::fixed(vec![id.to_vec()])
}
#[test]
fn ordered_source_keys_topological_groups_by_source() {
let a = gckey(b"a");
let b = gckey(b"b");
let c = gckey(b"c");
let parent_map = vec![
(a.clone(), vec![]),
(b.clone(), vec![a.clone()]),
(c.clone(), vec![b.clone()]),
];
let mut k2s = std::collections::HashMap::new();
k2s.insert(b.clone(), Source::Fallback(0));
let runs = ordered_source_keys("topological", &parent_map, &k2s);
assert_eq!(
runs,
vec![
(Source::Local, vec![a]),
(Source::Fallback(0), vec![b]),
(Source::Local, vec![c]),
]
);
}
#[test]
fn as_requested_source_keys_keeps_order_and_drops_absent() {
let a = gckey(b"a");
let b = gckey(b"b");
let absent = gckey(b"absent");
let f = gckey(b"f");
let locations: std::collections::HashSet<GcKey> = vec![a.clone()].into_iter().collect();
let unadded: std::collections::HashSet<GcKey> = vec![b.clone()].into_iter().collect();
let mut k2s = std::collections::HashMap::new();
k2s.insert(f.clone(), Source::Fallback(1));
let runs = as_requested_source_keys(
&[a.clone(), absent, b.clone(), f.clone()],
&locations,
&unadded,
&k2s,
);
assert_eq!(
runs,
vec![(Source::Local, vec![a, b]), (Source::Fallback(1), vec![f])]
);
}
#[test]
fn io_ordered_source_keys_unadded_first_then_grouped_then_fallbacks() {
let u = gckey(b"u");
let x = gckey(b"x");
let y = gckey(b"y");
let f = gckey(b"f");
let mut locations = std::collections::HashMap::new();
locations.insert(
x.clone(),
IndexMemo::new(ReadMemo::new("g2".to_string(), 0, 10), 0, 5),
);
locations.insert(
y.clone(),
IndexMemo::new(ReadMemo::new("g1".to_string(), 0, 10), 0, 5),
);
let runs = io_ordered_source_keys(
&[x.clone(), y.clone()],
&locations,
&[u.clone()],
vec![(Source::Fallback(0), vec![f.clone()])],
);
assert_eq!(
runs,
vec![
(Source::Local, vec![u, y, x]),
(Source::Fallback(0), vec![f]),
]
);
}
#[test]
fn batch_accumulator_queues_uncached_memos_and_counts_stop() {
let mut acc: BatchAccumulator<String> = BatchAccumulator::new();
let t1 = acc.add_key(gckey(b"k1"), ReadMemo::new("g1".into(), 0, 30), |_| false);
assert_eq!(t1, 30); let t2 = acc.add_key(gckey(b"k2"), ReadMemo::new("g2".into(), 0, 50), |_| false);
assert_eq!(t2, 80);
assert_eq!(acc.keys(), &[gckey(b"k1"), gckey(b"k2")]);
assert_eq!(
acc.memos_to_get(),
&[
ReadMemo::new("g1".into(), 0, 30),
ReadMemo::new("g2".into(), 0, 50),
]
);
}
#[test]
fn batch_accumulator_does_not_recount_repeated_memo() {
let mut acc: BatchAccumulator<String> = BatchAccumulator::new();
let block = ReadMemo::new("g1".to_string(), 0, 30);
acc.add_key(gckey(b"k1"), block.clone(), |_| false);
let total = acc.add_key(gckey(b"k2"), block.clone(), |_| false);
assert_eq!(total, 30);
assert_eq!(acc.keys().len(), 2);
assert_eq!(acc.memos_to_get(), &[block]);
}
#[test]
fn batch_accumulator_skips_fetch_for_cached_memo() {
let mut acc: BatchAccumulator<String> = BatchAccumulator::new();
let block = ReadMemo::new("g1".to_string(), 0, 30);
let total = acc.add_key(gckey(b"k1"), block, |_| true);
assert_eq!(total, 0);
assert!(acc.memos_to_get().is_empty());
assert_eq!(acc.keys().len(), 1);
}
#[test]
fn batch_accumulator_reset_clears_state() {
let mut acc: BatchAccumulator<String> = BatchAccumulator::new();
acc.add_key(gckey(b"k1"), ReadMemo::new("g1".into(), 0, 30), |_| false);
acc.reset();
assert!(acc.keys().is_empty());
assert!(acc.memos_to_get().is_empty());
assert_eq!(acc.total_bytes(), 0);
}
fn block_bytes(body: &[u8]) -> Vec<u8> {
let mut b = GroupCompressBlock::new();
b.set_content(body);
b.to_bytes()
}
struct MockAccess {
blocks: std::collections::HashMap<ReadMemo<String>, Vec<u8>>,
fetched: std::cell::RefCell<Vec<ReadMemo<String>>>,
}
impl GcAccess for MockAccess {
type F = String;
fn get_raw_records(
&self,
memos: &[ReadMemo<String>],
) -> Result<Vec<Vec<u8>>, crate::knit::KnitError> {
self.fetched.borrow_mut().extend_from_slice(memos);
memos
.iter()
.map(|m| {
self.blocks
.get(m)
.cloned()
.ok_or_else(|| crate::knit::KnitError::Corrupt("no such memo".into()))
})
.collect()
}
fn add_raw_record(
&self,
_size: usize,
_chunks: Vec<Vec<u8>>,
) -> Result<ReadMemo<String>, crate::knit::KnitError> {
unimplemented!("not needed for get_blocks tests")
}
}
struct UnusedIndex;
impl GcIndex for UnusedIndex {
type F = String;
fn get_build_details(
&self,
_keys: &[GcKey],
) -> Result<std::collections::HashMap<GcKey, GcBuildDetails<String>>, crate::knit::KnitError>
{
unimplemented!()
}
fn get_parent_map(
&self,
_keys: &[GcKey],
) -> Result<std::collections::HashMap<GcKey, Vec<GcKey>>, crate::knit::KnitError> {
unimplemented!()
}
fn keys(&self) -> Result<Vec<GcKey>, crate::knit::KnitError> {
unimplemented!()
}
fn has_graph(&self) -> bool {
true
}
fn check_write_ok(&self) -> Result<(), crate::knit::KnitError> {
unimplemented!()
}
fn add_records(
&self,
_records: &[(GcKey, IndexMemo<String>, Option<Vec<GcKey>>)],
_random_id: bool,
) -> Result<(), crate::knit::KnitError> {
unimplemented!()
}
}
#[test]
fn get_blocks_fetches_decodes_and_caches() {
let m1 = ReadMemo::new("g".to_string(), 0, 10);
let m2 = ReadMemo::new("g".to_string(), 10, 20);
let mut blocks = std::collections::HashMap::new();
blocks.insert(m1.clone(), block_bytes(b"first block body\n"));
blocks.insert(m2.clone(), block_bytes(b"second block body\n"));
let access = MockAccess {
blocks,
fetched: std::cell::RefCell::new(Vec::new()),
};
let vf = GroupCompressVersionedFiles::new(UnusedIndex, access, true);
let out = vf.get_blocks(&[m1.clone(), m2.clone()]).unwrap();
assert_eq!(out.len(), 2);
assert_eq!(out[0].0, m1);
assert_eq!(out[1].0, m2);
assert_eq!(vf.access().fetched.borrow().len(), 2);
let out2 = vf.get_blocks(&[m1.clone(), m2.clone()]).unwrap();
assert_eq!(out2.len(), 2);
assert_eq!(vf.access().fetched.borrow().len(), 2);
}
#[test]
fn get_blocks_dedups_repeated_memos_in_one_call() {
let m = ReadMemo::new("g".to_string(), 0, 10);
let mut blocks = std::collections::HashMap::new();
blocks.insert(m.clone(), block_bytes(b"body\n"));
let access = MockAccess {
blocks,
fetched: std::cell::RefCell::new(Vec::new()),
};
let vf = GroupCompressVersionedFiles::new(UnusedIndex, access, true);
let out = vf.get_blocks(&[m.clone(), m.clone()]).unwrap();
assert_eq!(out.len(), 2);
assert_eq!(vf.access().fetched.borrow().len(), 1);
}
struct MapIndex {
details: std::collections::HashMap<GcKey, GcBuildDetails<String>>,
}
impl GcIndex for MapIndex {
type F = String;
fn get_build_details(
&self,
keys: &[GcKey],
) -> Result<std::collections::HashMap<GcKey, GcBuildDetails<String>>, crate::knit::KnitError>
{
Ok(keys
.iter()
.filter_map(|k| self.details.get(k).map(|d| (k.clone(), d.clone())))
.collect())
}
fn get_parent_map(
&self,
keys: &[GcKey],
) -> Result<std::collections::HashMap<GcKey, Vec<GcKey>>, crate::knit::KnitError> {
Ok(keys
.iter()
.filter_map(|k| {
self.details
.get(k)
.map(|d| (k.clone(), d.parents.clone().unwrap_or_default()))
})
.collect())
}
fn keys(&self) -> Result<Vec<GcKey>, crate::knit::KnitError> {
Ok(self.details.keys().cloned().collect())
}
fn has_graph(&self) -> bool {
true
}
fn check_write_ok(&self) -> Result<(), crate::knit::KnitError> {
Ok(())
}
fn add_records(
&self,
_records: &[(GcKey, IndexMemo<String>, Option<Vec<GcKey>>)],
_random_id: bool,
) -> Result<(), crate::knit::KnitError> {
unimplemented!()
}
}
#[test]
fn get_record_stream_extracts_records_from_a_block() {
use crate::groupcompress::compressor::{GroupCompressor, RabinGroupCompressor};
let mut gc = RabinGroupCompressor::new(None);
let body_a = b"record a text line one\nrecord a text line two\n";
let body_b = b"record b text line one\nrecord b text line two\n";
let (_sha, a_start, a_end, _k) = gc
.compress(
&GcKey::fixed(vec![b"a".to_vec()]),
&[body_a.as_slice()],
body_a.len(),
None,
None,
None,
)
.unwrap();
let (_sha, b_start, b_end, _k) = gc
.compress(
&GcKey::fixed(vec![b"b".to_vec()]),
&[body_b.as_slice()],
body_b.len(),
None,
None,
None,
)
.unwrap();
let (content_chunks, content_len) = gc.flush();
let mut gcb = GroupCompressBlock::new();
gcb.set_chunked_content(&content_chunks, content_len);
let block: Vec<u8> = gcb.to_bytes();
let rm = ReadMemo::new("g".to_string(), 0, block.len() as u64);
let key_a = gckey(b"a");
let key_b = gckey(b"b");
let mut details = std::collections::HashMap::new();
details.insert(
key_a.clone(),
GcBuildDetails {
index_memo: IndexMemo::new(rm.clone(), a_start as u64, a_end as u64),
parents: Some(vec![]),
},
);
details.insert(
key_b.clone(),
GcBuildDetails {
index_memo: IndexMemo::new(rm.clone(), b_start as u64, b_end as u64),
parents: Some(vec![key_a.clone()]),
},
);
let mut blocks = std::collections::HashMap::new();
blocks.insert(rm, block);
let access = MockAccess {
blocks,
fetched: std::cell::RefCell::new(Vec::new()),
};
let vf = GroupCompressVersionedFiles::new(MapIndex { details }, access, true);
let records = vf
.get_record_stream(&[key_a.clone(), key_b.clone()], "as-requested")
.unwrap();
assert_eq!(records.len(), 2);
assert_eq!(records[0].key(), key_a);
assert_eq!(records[0].to_fulltext().as_ref(), body_a.as_slice());
assert_eq!(records[1].key(), key_b);
assert_eq!(records[1].to_fulltext().as_ref(), body_b.as_slice());
assert_eq!(records[1].parents(), Some(vec![key_a]));
}
#[test]
fn get_record_stream_yields_absent_for_missing_keys() {
let access = MockAccess {
blocks: std::collections::HashMap::new(),
fetched: std::cell::RefCell::new(Vec::new()),
};
let vf = GroupCompressVersionedFiles::new(
MapIndex {
details: std::collections::HashMap::new(),
},
access,
true,
);
let records = vf
.get_record_stream(&[gckey(b"nope")], "unordered")
.unwrap();
assert_eq!(records.len(), 1);
assert_eq!(records[0].storage_kind(), "absent");
assert_eq!(records[0].key(), gckey(b"nope"));
}
#[derive(Default)]
struct MemStore {
blocks: Vec<Vec<u8>>,
details: std::collections::HashMap<GcKey, GcBuildDetails<String>>,
}
#[derive(Clone)]
struct MemIndex(std::rc::Rc<std::cell::RefCell<MemStore>>);
#[derive(Clone)]
struct MemAccess(std::rc::Rc<std::cell::RefCell<MemStore>>);
impl GcIndex for MemIndex {
type F = String;
fn get_build_details(
&self,
keys: &[GcKey],
) -> Result<std::collections::HashMap<GcKey, GcBuildDetails<String>>, crate::knit::KnitError>
{
let store = self.0.borrow();
Ok(keys
.iter()
.filter_map(|k| store.details.get(k).map(|d| (k.clone(), d.clone())))
.collect())
}
fn get_parent_map(
&self,
keys: &[GcKey],
) -> Result<std::collections::HashMap<GcKey, Vec<GcKey>>, crate::knit::KnitError> {
let store = self.0.borrow();
Ok(keys
.iter()
.filter_map(|k| {
store
.details
.get(k)
.map(|d| (k.clone(), d.parents.clone().unwrap_or_default()))
})
.collect())
}
fn keys(&self) -> Result<Vec<GcKey>, crate::knit::KnitError> {
Ok(self.0.borrow().details.keys().cloned().collect())
}
fn has_graph(&self) -> bool {
true
}
fn check_write_ok(&self) -> Result<(), crate::knit::KnitError> {
Ok(())
}
fn add_records(
&self,
records: &[(GcKey, IndexMemo<String>, Option<Vec<GcKey>>)],
_random_id: bool,
) -> Result<(), crate::knit::KnitError> {
let mut store = self.0.borrow_mut();
for (key, memo, parents) in records {
store.details.insert(
key.clone(),
GcBuildDetails {
index_memo: memo.clone(),
parents: parents.clone(),
},
);
}
Ok(())
}
}
impl GcAccess for MemAccess {
type F = String;
fn get_raw_records(
&self,
memos: &[ReadMemo<String>],
) -> Result<Vec<Vec<u8>>, crate::knit::KnitError> {
let store = self.0.borrow();
memos
.iter()
.map(|m| {
let idx: usize = m
.index
.parse()
.map_err(|_| crate::knit::KnitError::Corrupt("bad block index".into()))?;
store
.blocks
.get(idx)
.cloned()
.ok_or_else(|| crate::knit::KnitError::Corrupt("no such block".into()))
})
.collect()
}
fn add_raw_record(
&self,
_size: usize,
chunks: Vec<Vec<u8>>,
) -> Result<ReadMemo<String>, crate::knit::KnitError> {
let mut store = self.0.borrow_mut();
let idx = store.blocks.len();
let bytes: Vec<u8> = chunks.concat();
let len = bytes.len() as u64;
store.blocks.push(bytes);
Ok(ReadMemo::new(idx.to_string(), 0, len))
}
}
#[test]
fn insert_record_stream_then_get_record_stream_round_trips() {
use crate::versionedfile::{ChunkedContentFactory, ContentFactory};
let store = std::rc::Rc::new(std::cell::RefCell::new(MemStore::default()));
let vf = GroupCompressVersionedFiles::new(
MemIndex(store.clone()),
MemAccess(store.clone()),
true,
);
let key_a = gckey(b"a");
let key_b = gckey(b"b");
let text_a = b"the quick brown fox\njumps over\n".to_vec();
let text_b = b"the lazy dog\nsleeps all day\n".to_vec();
let stream: Vec<Box<dyn ContentFactory>> = vec![
Box::new(ChunkedContentFactory::new(
None,
key_a.clone(),
Some(vec![]),
vec![text_a.clone()],
)),
Box::new(ChunkedContentFactory::new(
None,
key_b.clone(),
Some(vec![key_a.clone()]),
vec![text_b.clone()],
)),
];
let inserted = vf.insert_record_stream(stream, false).unwrap();
assert_eq!(inserted.len(), 2);
let records = vf
.get_record_stream(&[key_a.clone(), key_b.clone()], "as-requested")
.unwrap();
assert_eq!(records.len(), 2);
assert_eq!(records[0].key(), key_a);
assert_eq!(records[0].to_fulltext().as_ref(), text_a.as_slice());
assert_eq!(records[1].key(), key_b);
assert_eq!(records[1].to_fulltext().as_ref(), text_b.as_slice());
assert_eq!(records[1].parents(), Some(vec![key_a]));
}
#[test]
fn add_lines_then_get_sha1s_and_iter_lines() {
let store = std::rc::Rc::new(std::cell::RefCell::new(MemStore::default()));
let vf = GroupCompressVersionedFiles::new(
MemIndex(store.clone()),
MemAccess(store.clone()),
true,
);
let key = gckey(b"v1");
let lines = vec![b"alpha line\n".to_vec(), b"beta line\n".to_vec()];
let (sha1, length) = vf
.add_lines(key.clone(), Some(vec![]), lines.clone())
.unwrap();
assert_eq!(sha1, crate::weave::sha_strings(&lines));
assert_eq!(length, lines.iter().map(|l| l.len()).sum::<usize>());
let sha1s = vf.get_sha1s(&[key.clone()]).unwrap();
assert_eq!(sha1s.get(&key), Some(&sha1));
let iter_lines = vf
.iter_lines_added_or_present_in_keys(&[key.clone()])
.unwrap()
.collect::<Result<Vec<_>, _>>()
.unwrap();
assert_eq!(
iter_lines,
vec![
(b"alpha line\n".to_vec(), key.clone()),
(b"beta line\n".to_vec(), key.clone()),
]
);
vf.check().unwrap();
}
}