use hexane::PackError;
use std::collections::BTreeSet;
use std::{borrow::Cow, ops::Range};
use super::{parse, shift_range, ChunkType, Header, RawColumns};
use crate::change_graph::{ChangeGraph, ChangeGraphCols};
use crate::op_set2::change::{ChangeCollector, CollectedChanges, OutOfMemory};
use crate::op_set2::{OpSet, ReadOpError};
use crate::storage::columns::compression::Uncompressed;
use crate::storage::ColumnSpec;
use crate::{ActorId, Automerge, Change, ChangeHash, TextEncoding};
mod compression;
#[allow(dead_code)]
pub(crate) enum CompressConfig {
None,
Threshold(usize),
}
#[derive(Debug, Clone)]
pub(crate) struct Document<'a> {
bytes: Cow<'a, [u8]>,
#[allow(dead_code)]
compressed_bytes: Option<Cow<'a, [u8]>>,
header: Header,
actors: Vec<ActorId>,
heads: Vec<ChangeHash>,
pub(crate) op_metadata: RawColumns<Uncompressed>,
op_bytes: Range<usize>,
change_metadata: RawColumns<Uncompressed>,
change_bytes: Range<usize>,
}
#[derive(thiserror::Error, Debug)]
pub(crate) enum ParseError {
#[error(transparent)]
Leb128(#[from] parse::leb128::Error),
#[error(transparent)]
RawColumns(#[from] crate::storage::columns::raw_column::ParseError),
#[error("bad column layout for {column_type}s: {error}")]
BadColumnLayout {
column_type: &'static str,
error: super::columns::BadColumnLayout,
},
}
impl<'a> Document<'a> {
pub(crate) fn parse(
input: parse::Input<'a>,
header: Header,
) -> parse::ParseResult<'a, Document<'a>, ParseError> {
let i = input;
let (i, r) = parse::range_of(
|i| -> parse::ParseResult<'_, _, ParseError> {
let (i, actors) = parse::length_prefixed(parse::actor_id)(i)?;
let (i, heads) = parse::length_prefixed(parse::change_hash)(i)?;
let (i, change_meta) = RawColumns::parse::<ParseError>(i)?;
let (i, ops_meta) = RawColumns::parse::<ParseError>(i)?;
Ok((i, (actors, heads, change_meta, ops_meta)))
},
i,
)?;
let prefix = r.range.start;
let (actors, heads, change_meta, ops_meta) = r.value;
let change_len = change_meta.total_column_len();
let (i, changes) = parse::range_only(|i| parse::take_n(change_len, i), i)?;
let ops_len = ops_meta.total_column_len();
let (i, ops) = parse::range_only(|i| parse::take_n(ops_len, i), i)?;
let (i, Range { start: suffix, .. }) = parse::range_only_unless_empty(
|i| parse::apply_n(heads.len(), parse::leb128_u64::<ParseError>)(i),
i,
)?;
let compression::Decompressed {
change_bytes,
op_bytes,
uncompressed,
compressed,
changes,
ops,
} = compression::decompress(compression::Args {
prefix,
suffix,
original: Cow::Borrowed(input.bytes()),
changes: compression::Cols::new(changes, change_meta),
ops: compression::Cols::new(ops, ops_meta),
extra_args: (),
})
.map_err(|e| parse::ParseError::Error(ParseError::RawColumns(e)))?;
let op_metadata = OpSet::validate(op_bytes.len(), &ops).map_err(|error| {
parse::ParseError::Error(ParseError::BadColumnLayout {
column_type: "ops",
error,
})
})?;
let change_metadata =
ChangeGraph::validate(change_bytes.len(), &changes).map_err(|error| {
parse::ParseError::Error(ParseError::BadColumnLayout {
column_type: "changes",
error,
})
})?;
Ok((
i,
Document {
bytes: uncompressed,
compressed_bytes: compressed,
header,
actors,
heads,
op_metadata,
op_bytes,
change_metadata,
change_bytes,
},
))
}
pub(crate) fn change_meta(&self) -> &RawColumns<Uncompressed> {
&self.change_metadata
}
pub(crate) fn change_bytes(&self) -> &[u8] {
&self.bytes[self.change_bytes.clone()]
}
pub(crate) fn new(
op_set: &OpSet,
change_graph: &ChangeGraph,
compress: CompressConfig,
) -> Document<'static> {
let (op_metadata, ops_out_b) = op_set.export();
let mut change_out = Vec::new();
let change_metadata = change_graph.encode(&mut change_out);
let actors = op_set.actors.clone();
let mut data = Vec::with_capacity(ops_out_b.len() + change_out.len());
leb128::write::unsigned(&mut data, actors.len() as u64).unwrap();
for actor in &actors {
leb128::write::unsigned(&mut data, actor.to_bytes().len() as u64).unwrap();
data.extend(actor.to_bytes());
}
let heads = change_graph.heads().collect::<Vec<_>>();
let head_indices = change_graph.head_indexes().collect::<Vec<_>>();
leb128::write::unsigned(&mut data, heads.len() as u64).unwrap();
for head in &heads {
data.extend(head.as_bytes());
}
let prefix_len = data.len();
change_metadata.write(&mut data);
op_metadata.write(&mut data);
let change_start = data.len();
let change_end = change_start + change_out.len();
data.extend(change_out);
let ops_start = data.len();
let ops_end = ops_start + ops_out_b.len();
data.extend(ops_out_b);
let suffix_start = data.len();
for index in &head_indices {
leb128::write::unsigned(&mut data, *index).unwrap();
}
let header = Header::new(ChunkType::Document, &data);
let mut bytes = Vec::with_capacity(data.len() + header.len());
header.write(&mut bytes);
let header_len = bytes.len();
bytes.extend(&data);
let op_bytes = shift_range(ops_start..ops_end, header.len());
let change_bytes = shift_range(change_start..change_end, header.len());
let compressed_bytes = if let CompressConfig::Threshold(threshold) = compress {
let compressed = Cow::Owned(compression::compress(compression::Args {
prefix: prefix_len + header.len(),
suffix: suffix_start + header.len(),
ops: compression::Cols {
raw_columns: op_metadata.clone(),
data: op_bytes.clone(),
},
changes: compression::Cols {
raw_columns: change_metadata.clone(),
data: change_bytes.clone(),
},
original: Cow::Borrowed(&bytes),
extra_args: compression::CompressArgs {
threshold,
original_header_len: header_len,
},
}));
Some(compressed)
} else {
None
};
Document {
actors,
bytes: Cow::Owned(bytes),
compressed_bytes,
header,
heads,
op_metadata,
op_bytes,
change_metadata,
change_bytes,
}
}
pub(crate) fn op_raw_bytes(&self) -> &[u8] {
&self.bytes[self.op_bytes.clone()]
}
pub(crate) fn into_bytes(self) -> Vec<u8> {
if let Some(compressed) = self.compressed_bytes {
compressed.into_owned()
} else {
self.bytes.into_owned()
}
}
pub(crate) fn checksum_valid(&self) -> bool {
self.header.checksum_valid()
}
pub(crate) fn actors(&self) -> &[ActorId] {
&self.actors
}
pub(crate) fn heads(&self) -> &[ChangeHash] {
&self.heads
}
fn verify_changes(
&self,
cc: &CollectedChanges,
mode: VerificationMode,
) -> Result<(), ReconstructError> {
if mode == VerificationMode::Check && !self.heads().iter().eq(cc.heads.iter()) {
let expected_heads: BTreeSet<_> = self.heads().iter().cloned().collect();
tracing::error!(?expected_heads, ?cc.heads, "mismatching heads");
Err(ReconstructError::MismatchingHeads(MismatchedHeads {
changes: cc.changes.clone(),
expected_heads,
derived_heads: cc.heads.clone(),
}))
} else {
Ok(())
}
}
pub(crate) fn reconstruct(
&self,
mode: VerificationMode,
text_encoding: TextEncoding,
) -> Result<Automerge, ReconstructError> {
let mut op_set = OpSet::load(self, text_encoding)?;
let change_cols = ChangeGraphCols::load(self)?;
let mut index = op_set.index_builder();
let change_collector = ChangeCollector::try_new(&change_cols, &op_set)?;
let mut change_collector = change_collector.with_index(&mut index);
change_collector.process_ops(&op_set)?;
let changes = change_collector.collect(&op_set)?;
self.verify_changes(&changes, mode)?;
op_set.set_indexes(index);
let change_graph = change_cols.finalize(&changes.changes);
debug_assert_eq!(changes.changes.len(), change_graph.len());
debug_assert!(op_set.validate_top_index());
Ok(Automerge::from_parts(op_set, change_graph))
}
pub(crate) fn reconstruct_changes(
&self,
text_encoding: TextEncoding,
) -> Result<Vec<Change>, ReconstructError> {
let op_set = OpSet::load(self, text_encoding)?;
let change_cols = ChangeGraphCols::load(self)?;
let mut change_collector = ChangeCollector::try_new(&change_cols, &op_set)?;
change_collector.process_ops(&op_set)?;
Ok(change_collector.collect(&op_set)?.changes)
}
}
#[derive(Debug, thiserror::Error)]
pub(crate) enum ReconstructError {
#[error("invalid changes: {0}")]
InvalidChanges(#[from] crate::storage::load::change_collector::Error),
#[error("mismatching heads")]
MismatchingHeads(MismatchedHeads),
#[error(transparent)]
InvalidOp(#[from] crate::error::InvalidOpType),
#[error(transparent)]
PackErr(#[from] PackError),
#[error(transparent)]
ReadOpErr(#[from] ReadOpError),
#[error("invalid actor id {0}")]
InvalidActorId(usize),
#[error("invalid column length {0:?}")]
InvalidColumnLength(ColumnSpec),
#[error("max_op is lower than start_op")]
InvalidMaxOp,
#[error(transparent)]
OutOfMemory(#[from] OutOfMemory),
}
pub(crate) struct MismatchedHeads {
changes: Vec<Change>,
expected_heads: BTreeSet<ChangeHash>,
derived_heads: BTreeSet<ChangeHash>,
}
impl std::fmt::Debug for MismatchedHeads {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("MismatchedHeads")
.field("changes", &self.changes.len())
.field("expected_heads", &self.expected_heads)
.field("derived_heads", &self.derived_heads)
.finish()
}
}
use super::load::VerificationMode;