use std::panic::{catch_unwind, AssertUnwindSafe};
use loro::{EncodedBlobMode, ExportMode, LoroDoc, LoroValue, VersionVector};
use crate::error::{DataError, DataResult};
use crate::policy;
use crate::value::DataValue;
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct VersionToken(Vec<u8>);
impl VersionToken {
pub fn as_bytes(&self) -> &[u8] {
&self.0
}
pub fn from_bytes(bytes: impl Into<Vec<u8>>) -> Self {
Self(bytes.into())
}
pub fn includes(&self, other: &VersionToken) -> DataResult<bool> {
Ok(decode_version(&self.0)?.includes_vv(&decode_version(&other.0)?))
}
pub fn union(&self, other: &VersionToken) -> DataResult<VersionToken> {
let mut ours = decode_version(&self.0)?;
ours.merge(&decode_version(&other.0)?);
Ok(Self(ours.encode()))
}
pub fn validate(&self) -> DataResult<()> {
decode_version(&self.0).map(|_| ())
}
}
fn decode_version(bytes: &[u8]) -> DataResult<VersionVector> {
VersionVector::decode(bytes).map_err(|err| DataError::Corrupt(err.to_string()))
}
pub fn blob_end_version(bytes: &[u8]) -> DataResult<VersionToken> {
let meta = LoroDoc::decode_import_blob_meta(bytes, true)
.map_err(|err| DataError::Corrupt(err.to_string()))?;
Ok(VersionToken(meta.partial_end_vv.encode()))
}
#[derive(Debug, Clone)]
pub struct Delta {
pub bytes: Vec<u8>,
pub version: VersionToken,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct DocStats {
pub compacted_bytes: usize,
pub commits_since_compaction: u32,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ImportOutcome {
Applied,
Parked,
}
impl ImportOutcome {
pub fn is_parked(self) -> bool {
matches!(self, Self::Parked)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct BlobMeta {
already_applied: bool,
spans_trimmed_history: bool,
is_snapshot: bool,
change_count: u32,
}
impl BlobMeta {
pub fn already_applied(self) -> bool {
self.already_applied
}
pub fn spans_trimmed_history(self) -> bool {
self.spans_trimmed_history
}
pub fn is_snapshot(self) -> bool {
self.is_snapshot
}
pub fn change_count(self) -> u32 {
self.change_count
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum RemoteImport {
Applied,
Parked,
AlreadyHave,
RefusedTrimmedHistory,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum CatchUp {
UpToDate,
Updates(Vec<u8>),
NeedsSnapshot,
}
pub struct DataDoc {
inner: LoroDoc,
exported: VersionVector,
previous_exported: Option<VersionVector>,
poisoned: bool,
commits_since_compaction: u32,
last_measured_bytes: usize,
delta_bytes_since_measure: usize,
}
impl Default for DataDoc {
fn default() -> Self {
Self::new()
}
}
impl DataDoc {
pub fn new() -> Self {
Self {
inner: LoroDoc::new(),
exported: VersionVector::new(),
previous_exported: None,
poisoned: false,
commits_since_compaction: 0,
last_measured_bytes: 0,
delta_bytes_since_measure: 0,
}
}
pub fn is_poisoned(&self) -> bool {
self.poisoned
}
fn check_live(&self) -> DataResult<()> {
if self.poisoned {
return Err(DataError::Poisoned);
}
Ok(())
}
pub fn import(&mut self, bytes: &[u8]) -> DataResult<ImportOutcome> {
self.check_live()?;
if let Err(err) = LoroDoc::decode_import_blob_meta(bytes, true) {
return Err(DataError::Corrupt(err.to_string()));
}
let outcome = catch_unwind(AssertUnwindSafe(|| self.inner.import(bytes)));
match outcome {
Ok(Ok(status)) => Ok(match status.pending {
Some(range) if !range.is_empty() => ImportOutcome::Parked,
_ => ImportOutcome::Applied,
}),
Ok(Err(err)) => Err(DataError::Corrupt(err.to_string())),
Err(_) => {
self.poisoned = true;
Err(DataError::Corrupt(
"engine panicked while importing".to_string(),
))
}
}
}
pub fn inspect(&self, bytes: &[u8]) -> DataResult<BlobMeta> {
self.check_live()?;
let meta = LoroDoc::decode_import_blob_meta(bytes, true)
.map_err(|err| DataError::Corrupt(err.to_string()))?;
let is_snapshot = matches!(
meta.mode,
EncodedBlobMode::Snapshot
| EncodedBlobMode::OutdatedSnapshot
| EncodedBlobMode::ShallowSnapshot
);
let already_applied = self.inner.oplog_vv().includes_vv(&meta.partial_end_vv);
let shallow_since = self.inner.shallow_since_vv();
let spans_trimmed_history = if shallow_since.is_empty() {
false
} else if is_snapshot {
!meta.partial_end_vv.includes_vv(&self.inner.oplog_vv())
} else {
shallow_since.to_vv().includes_vv(&meta.partial_start_vv)
};
Ok(BlobMeta {
already_applied,
spans_trimmed_history,
is_snapshot,
change_count: meta.change_num,
})
}
pub fn import_remote(&mut self, bytes: &[u8]) -> DataResult<RemoteImport> {
let meta = self.inspect(bytes)?;
if meta.already_applied() {
return Ok(RemoteImport::AlreadyHave);
}
if meta.spans_trimmed_history() {
return Ok(RemoteImport::RefusedTrimmedHistory);
}
Ok(match self.import(bytes)? {
ImportOutcome::Applied => RemoteImport::Applied,
ImportOutcome::Parked => RemoteImport::Parked,
})
}
pub fn export_since(&self, from: &VersionToken) -> DataResult<CatchUp> {
self.check_live()?;
let from_vv = VersionVector::decode(from.as_bytes())
.map_err(|err| DataError::Corrupt(err.to_string()))?;
if from_vv.includes_vv(&self.inner.oplog_vv()) {
return Ok(CatchUp::UpToDate);
}
let shallow_since = self.inner.shallow_since_vv();
if !shallow_since.is_empty() && !from_vv.includes_vv(&shallow_since.to_vv()) {
return Ok(CatchUp::NeedsSnapshot);
}
self.inner
.export(ExportMode::updates(&from_vv))
.map(CatchUp::Updates)
.map_err(|err| DataError::Engine(err.to_string()))
}
pub fn mark_persisted(&mut self) {
self.exported = self.inner.oplog_vv();
}
pub fn commit(&mut self) -> DataResult<Option<Delta>> {
self.check_live()?;
self.inner.commit();
let current = self.inner.oplog_vv();
if current == self.exported {
return Ok(None);
}
let bytes = self
.inner
.export(ExportMode::updates(&self.exported))
.map_err(|err| DataError::Engine(err.to_string()))?;
self.previous_exported = Some(std::mem::replace(&mut self.exported, current));
self.commits_since_compaction = self.commits_since_compaction.saturating_add(1);
self.delta_bytes_since_measure = self.delta_bytes_since_measure.saturating_add(bytes.len());
Ok(Some(Delta {
bytes,
version: self.version(),
}))
}
pub fn rewind_last_commit(&mut self) {
if let Some(previous) = self.previous_exported.take() {
self.exported = previous;
self.commits_since_compaction = self.commits_since_compaction.saturating_sub(1);
}
}
pub fn version(&self) -> VersionToken {
VersionToken(self.inner.oplog_vv().encode())
}
pub fn export_compacted(&self) -> DataResult<Vec<u8>> {
self.check_live()?;
let frontiers = self.inner.oplog_frontiers();
self.inner
.export(ExportMode::shallow_snapshot(&frontiers))
.map_err(|err| DataError::Engine(err.to_string()))
}
pub fn export_raw(&self) -> DataResult<Vec<u8>> {
self.check_live()?;
self.inner
.export(ExportMode::Snapshot)
.map_err(|err| DataError::Engine(err.to_string()))
}
pub fn export_json(&self) -> DataResult<String> {
self.check_live()?;
let value = self.inner.get_deep_value();
serde_json::to_string(&value).map_err(|err| DataError::Engine(err.to_string()))
}
pub fn measure(&mut self) -> DataResult<usize> {
let bytes = self.export_compacted()?.len();
self.last_measured_bytes = bytes;
self.delta_bytes_since_measure = 0;
Ok(bytes)
}
pub fn check_size(&mut self) -> DataResult<(policy::SizeVerdict, Option<usize>)> {
if !policy::needs_size_check(self.last_measured_bytes, self.delta_bytes_since_measure) {
return Ok((policy::SizeVerdict::Ok, None));
}
let bytes = self.measure()?;
Ok((policy::size_verdict(bytes), Some(bytes)))
}
pub fn stats(&mut self) -> DataResult<DocStats> {
let compacted_bytes = self.measure()?;
Ok(DocStats {
compacted_bytes,
commits_since_compaction: self.commits_since_compaction,
})
}
pub fn commits_since_compaction(&self) -> u32 {
self.commits_since_compaction
}
pub fn mark_compacted(&mut self, compacted_bytes: usize) {
self.commits_since_compaction = 0;
self.last_measured_bytes = compacted_bytes;
self.delta_bytes_since_measure = 0;
}
pub fn restore_bookkeeping(&mut self, compacted_bytes: usize, log_bytes: usize, commits: u32) {
self.last_measured_bytes = compacted_bytes;
self.delta_bytes_since_measure = log_bytes;
self.commits_since_compaction = commits;
}
pub fn map_set(&mut self, collection: &str, key: &str, value: DataValue) -> DataResult<()> {
self.check_live()?;
crate::validate_name(collection)?;
crate::validate_key(key)?;
crate::validate_value(&value)?;
let map = self.inner.get_map(collection);
map.insert(key, to_engine_value(value))
.map_err(|err| DataError::Engine(err.to_string()))
}
pub fn map_delete(&mut self, collection: &str, key: &str) -> DataResult<()> {
self.check_live()?;
crate::validate_name(collection)?;
let map = self.inner.get_map(collection);
map.delete(key)
.map_err(|err| DataError::Engine(err.to_string()))
}
pub fn map_get(&self, collection: &str, key: &str) -> DataResult<Option<DataValue>> {
self.check_live()?;
crate::validate_name(collection)?;
let map = self.inner.get_map(collection);
Ok(map.get(key).and_then(|entry| match entry {
loro::ValueOrContainer::Value(value) => from_engine_value(&value),
loro::ValueOrContainer::Container(_) => None,
}))
}
pub fn list_push(&mut self, collection: &str, value: DataValue) -> DataResult<()> {
self.check_live()?;
crate::validate_name(collection)?;
crate::validate_value(&value)?;
let list = self.inner.get_list(collection);
list.push(to_engine_value(value))
.map_err(|err| DataError::Engine(err.to_string()))
}
pub fn list_insert(
&mut self,
collection: &str,
index: usize,
value: DataValue,
) -> DataResult<()> {
self.check_live()?;
crate::validate_name(collection)?;
crate::validate_value(&value)?;
let list = self.inner.get_list(collection);
let length = list.len();
if index > length {
return Err(DataError::OutOfRange {
position: index,
length,
});
}
list.insert(index, to_engine_value(value))
.map_err(|err| DataError::Engine(err.to_string()))
}
pub fn list_delete(&mut self, collection: &str, index: usize, count: usize) -> DataResult<()> {
self.check_live()?;
crate::validate_name(collection)?;
let list = self.inner.get_list(collection);
let length = list.len();
if index.saturating_add(count) > length {
return Err(DataError::OutOfRange {
position: index.saturating_add(count),
length,
});
}
list.delete(index, count)
.map_err(|err| DataError::Engine(err.to_string()))
}
pub fn list_len(&self, collection: &str) -> DataResult<usize> {
self.check_live()?;
crate::validate_name(collection)?;
Ok(self.inner.get_list(collection).len())
}
pub fn text_insert(&mut self, collection: &str, position: usize, text: &str) -> DataResult<()> {
self.check_live()?;
crate::validate_name(collection)?;
crate::validate_value_len(text.len())?;
let handle = self.inner.get_text(collection);
let length = handle.len_unicode();
if position > length {
return Err(DataError::OutOfRange { position, length });
}
handle
.insert(position, text)
.map_err(|err| DataError::Engine(err.to_string()))
}
pub fn text_delete(
&mut self,
collection: &str,
position: usize,
count: usize,
) -> DataResult<()> {
self.check_live()?;
crate::validate_name(collection)?;
let handle = self.inner.get_text(collection);
let length = handle.len_unicode();
if position.saturating_add(count) > length {
return Err(DataError::OutOfRange {
position: position.saturating_add(count),
length,
});
}
handle
.delete(position, count)
.map_err(|err| DataError::Engine(err.to_string()))
}
pub fn text_value(&self, collection: &str) -> DataResult<String> {
self.check_live()?;
crate::validate_name(collection)?;
Ok(self.inner.get_text(collection).to_string())
}
pub fn counter_increment(&mut self, collection: &str, amount: f64) -> DataResult<()> {
self.check_live()?;
crate::validate_name(collection)?;
self.inner
.get_counter(collection)
.increment(amount)
.map_err(|err| DataError::Engine(err.to_string()))
}
pub fn counter_value(&self, collection: &str) -> DataResult<f64> {
self.check_live()?;
crate::validate_name(collection)?;
Ok(self.inner.get_counter(collection).get_value())
}
}
impl std::fmt::Debug for DataDoc {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("DataDoc")
.field("poisoned", &self.poisoned)
.field("commits_since_compaction", &self.commits_since_compaction)
.finish_non_exhaustive()
}
}
const ATTACHMENT_TAG_KEY: &str = "kind";
const ATTACHMENT_TAG: &str = "attachment";
const ATTACHMENT_HASH_KEY: &str = "hash";
const ATTACHMENT_SIZE_KEY: &str = "size";
const ATTACHMENT_NAME_KEY: &str = "name";
const ATTACHMENT_MIME_KEY: &str = "mime";
fn to_engine_value(value: DataValue) -> LoroValue {
match value {
DataValue::Null => LoroValue::Null,
DataValue::Bool { value } => LoroValue::from(value),
DataValue::Int { value } => LoroValue::from(value),
DataValue::Float { value } => LoroValue::from(value),
DataValue::Text { value } => LoroValue::from(value),
DataValue::Bytes { value } => LoroValue::from(value),
DataValue::Attachment {
hash,
size,
name,
mime,
} => {
let mut fields: Vec<(String, LoroValue)> = vec![
(
ATTACHMENT_TAG_KEY.to_string(),
LoroValue::from(ATTACHMENT_TAG),
),
(ATTACHMENT_HASH_KEY.to_string(), LoroValue::from(hash)),
(
ATTACHMENT_SIZE_KEY.to_string(),
LoroValue::from(i64::try_from(size).unwrap_or(i64::MAX)),
),
];
if let Some(name) = name {
fields.push((ATTACHMENT_NAME_KEY.to_string(), LoroValue::from(name)));
}
if let Some(mime) = mime {
fields.push((ATTACHMENT_MIME_KEY.to_string(), LoroValue::from(mime)));
}
LoroValue::Map(fields.into())
}
}
}
fn from_engine_value(value: &LoroValue) -> Option<DataValue> {
match value {
LoroValue::Null => Some(DataValue::Null),
LoroValue::Bool(inner) => Some(DataValue::bool(*inner)),
LoroValue::I64(inner) => Some(DataValue::int(*inner)),
LoroValue::Double(inner) => Some(DataValue::float(*inner)),
LoroValue::String(inner) => Some(DataValue::text(inner.to_string())),
LoroValue::Binary(inner) => Some(DataValue::bytes(inner.to_vec())),
LoroValue::Map(fields) => {
match fields.get(ATTACHMENT_TAG_KEY) {
Some(LoroValue::String(tag)) if tag.as_str() == ATTACHMENT_TAG => {}
_ => return None,
}
let hash = match fields.get(ATTACHMENT_HASH_KEY) {
Some(LoroValue::String(hash)) => hash.to_string(),
_ => return None,
};
let size = match fields.get(ATTACHMENT_SIZE_KEY) {
Some(LoroValue::I64(size)) if *size >= 0 => *size as u64,
_ => return None,
};
let text_field = |key: &str| match fields.get(key) {
Some(LoroValue::String(value)) => Ok(Some(value.to_string())),
None => Ok(None),
Some(_) => Err(()),
};
let (Ok(name), Ok(mime)) = (
text_field(ATTACHMENT_NAME_KEY),
text_field(ATTACHMENT_MIME_KEY),
) else {
return None;
};
if crate::validate_attachment(&hash, size, name.as_deref(), mime.as_deref()).is_err() {
return None;
}
Some(DataValue::Attachment {
hash,
size,
name,
mime,
})
}
_ => None,
}
}
#[cfg(test)]
mod engine_value_tests {
use super::*;
const TEST_HASH: &str = "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855";
fn tagged(tag: Option<&str>) -> LoroValue {
let mut fields = vec![
(ATTACHMENT_HASH_KEY.to_string(), LoroValue::from(TEST_HASH)),
(ATTACHMENT_SIZE_KEY.to_string(), LoroValue::from(4096i64)),
];
if let Some(tag) = tag {
fields.push((ATTACHMENT_TAG_KEY.to_string(), LoroValue::from(tag)));
}
LoroValue::Map(fields.into())
}
#[test]
fn a_map_without_the_attachment_tag_reads_as_absent() {
let cases = [
tagged(None),
tagged(Some("something-later")),
tagged(Some("Attachment")),
];
for case in cases {
assert_eq!(
from_engine_value(&case),
None,
"{case:?} must read as absent"
);
}
assert!(matches!(
from_engine_value(&tagged(Some(ATTACHMENT_TAG))),
Some(DataValue::Attachment { .. })
));
}
#[test]
fn an_attachment_without_an_address_reads_as_absent() {
let no_hash = LoroValue::Map(
vec![
(
ATTACHMENT_TAG_KEY.to_string(),
LoroValue::from(ATTACHMENT_TAG),
),
(ATTACHMENT_SIZE_KEY.to_string(), LoroValue::from(4096i64)),
]
.into(),
);
assert_eq!(from_engine_value(&no_hash), None);
}
#[test]
fn a_display_field_of_the_wrong_type_makes_the_reference_absent() {
for wrong in [ATTACHMENT_NAME_KEY, ATTACHMENT_MIME_KEY] {
let value = LoroValue::Map(
vec![
(
ATTACHMENT_TAG_KEY.to_string(),
LoroValue::from(ATTACHMENT_TAG),
),
(ATTACHMENT_HASH_KEY.to_string(), LoroValue::from(TEST_HASH)),
(ATTACHMENT_SIZE_KEY.to_string(), LoroValue::from(4096i64)),
(wrong.to_string(), LoroValue::from(5i64)),
]
.into(),
);
assert_eq!(
from_engine_value(&value),
None,
"a {wrong} field of the wrong type must make the whole reference absent"
);
}
let good = LoroValue::Map(
vec![
(
ATTACHMENT_TAG_KEY.to_string(),
LoroValue::from(ATTACHMENT_TAG),
),
(ATTACHMENT_HASH_KEY.to_string(), LoroValue::from(TEST_HASH)),
(ATTACHMENT_SIZE_KEY.to_string(), LoroValue::from(4096i64)),
(ATTACHMENT_NAME_KEY.to_string(), LoroValue::from("plan.pdf")),
]
.into(),
);
assert!(matches!(
from_engine_value(&good),
Some(DataValue::Attachment { .. })
));
}
#[test]
fn a_field_this_version_does_not_know_is_ignored() {
let mut fields = vec![
(
ATTACHMENT_TAG_KEY.to_string(),
LoroValue::from(ATTACHMENT_TAG),
),
(ATTACHMENT_HASH_KEY.to_string(), LoroValue::from(TEST_HASH)),
(ATTACHMENT_SIZE_KEY.to_string(), LoroValue::from(4096i64)),
];
fields.push(("something_later".to_string(), LoroValue::from("v2 field")));
assert!(matches!(
from_engine_value(&LoroValue::Map(fields.into())),
Some(DataValue::Attachment { .. })
));
}
#[test]
fn an_attachment_survives_the_engine_conversion_both_ways() {
let reference = DataValue::Attachment {
hash: "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855".to_string(),
size: 4096,
name: Some("plan.pdf".to_string()),
mime: Some("application/pdf".to_string()),
};
let engine = to_engine_value(reference.clone());
assert_eq!(from_engine_value(&engine), Some(reference));
}
#[test]
fn a_negative_size_is_refused_rather_than_wrapped() {
let engine = LoroValue::Map(
vec![
(
ATTACHMENT_TAG_KEY.to_string(),
LoroValue::from(ATTACHMENT_TAG),
),
(
ATTACHMENT_HASH_KEY.to_string(),
LoroValue::from(
"e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855",
),
),
(ATTACHMENT_SIZE_KEY.to_string(), LoroValue::from(-1i64)),
]
.into(),
);
assert_eq!(from_engine_value(&engine), None);
}
}