827 lines
27 KiB
Rust
827 lines
27 KiB
Rust
/*
|
|
* Copyright (C) 2026 Mateusz Gruszczynski @linuxiarz.pl
|
|
* Source-Available Code / Dual-Licensed.
|
|
*
|
|
* Free for non-commercial and evaluation use under terms of BSL/GPLv3.
|
|
* Commercial or production use requires a valid paid license.
|
|
* See LICENSE file in repository root for details.
|
|
*/
|
|
|
|
use serde::{Deserialize, Serialize};
|
|
use std::{
|
|
collections::{HashMap, VecDeque},
|
|
error::Error,
|
|
fmt,
|
|
};
|
|
|
|
pub const MAX_OPERATION_COMPONENTS: usize = 4096;
|
|
const MAX_OPERATION_OWNER_SPANS: usize = 8192;
|
|
const MAX_OPERATION_INSERT_BYTES: usize = 2_000_000;
|
|
const MAX_OWNER_LENGTH: usize = 120;
|
|
const MAX_OPERATION_HISTORY: usize = 512;
|
|
const MAX_OPERATION_HISTORY_BYTES: usize = 8 * 1024 * 1024;
|
|
const AUTHORSHIP_VERSION: u8 = 2;
|
|
const OWNER_COLOR_SEPARATOR: char = '\u{001f}';
|
|
|
|
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
|
|
pub struct OwnerSpan {
|
|
pub start: usize,
|
|
pub end: usize,
|
|
pub owner: String,
|
|
}
|
|
|
|
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
|
|
#[serde(tag = "kind", rename_all = "snake_case")]
|
|
pub enum OperationComponent {
|
|
Retain {
|
|
count: usize,
|
|
},
|
|
Delete {
|
|
count: usize,
|
|
},
|
|
Insert {
|
|
text: String,
|
|
#[serde(default)]
|
|
owners: Vec<OwnerSpan>,
|
|
},
|
|
}
|
|
|
|
#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq, Eq)]
|
|
pub struct TextOperation {
|
|
#[serde(default)]
|
|
pub components: Vec<OperationComponent>,
|
|
}
|
|
|
|
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
|
|
pub struct OwnerReplacement {
|
|
pub owner: String,
|
|
pub replacement: String,
|
|
}
|
|
|
|
#[derive(Debug, Clone, Serialize)]
|
|
pub struct AppliedOperation {
|
|
pub base_revision_id: i64,
|
|
pub revision_id: i64,
|
|
pub client_id: String,
|
|
pub update_id: u64,
|
|
pub operation: TextOperation,
|
|
pub owner_replacements: Vec<OwnerReplacement>,
|
|
}
|
|
|
|
#[derive(Debug)]
|
|
pub struct CollaborativeDocument {
|
|
pub content: String,
|
|
pub owner_map: String,
|
|
pub revision_id: i64,
|
|
history: VecDeque<AppliedOperation>,
|
|
history_bytes: usize,
|
|
acknowledged_updates: HashMap<String, u64>,
|
|
}
|
|
|
|
impl CollaborativeDocument {
|
|
pub fn new(content: String, owner_map: String, revision_id: i64) -> Self {
|
|
Self {
|
|
content,
|
|
owner_map,
|
|
revision_id,
|
|
history: VecDeque::new(),
|
|
history_bytes: 0,
|
|
acknowledged_updates: HashMap::new(),
|
|
}
|
|
}
|
|
|
|
pub fn transform_from(
|
|
&self,
|
|
base_revision_id: i64,
|
|
operation: &TextOperation,
|
|
client_id: &str,
|
|
update_id: u64,
|
|
) -> Result<TextOperation, OperationError> {
|
|
let mut transformed = normalize_operation(operation)?;
|
|
if base_revision_id == self.revision_id {
|
|
return Ok(transformed);
|
|
}
|
|
|
|
let Some(start) = self
|
|
.history
|
|
.iter()
|
|
.position(|entry| entry.base_revision_id == base_revision_id)
|
|
else {
|
|
return Err(OperationError::RevisionUnavailable);
|
|
};
|
|
|
|
let mut expected_revision = base_revision_id;
|
|
for applied in self.history.iter().skip(start) {
|
|
if applied.base_revision_id != expected_revision {
|
|
return Err(OperationError::RevisionUnavailable);
|
|
}
|
|
let incoming_has_priority =
|
|
operation_key_before(client_id, update_id, &applied.client_id, applied.update_id);
|
|
transformed =
|
|
transform_operation(&transformed, &applied.operation, incoming_has_priority)?;
|
|
expected_revision = applied.revision_id;
|
|
if expected_revision == self.revision_id {
|
|
return Ok(transformed);
|
|
}
|
|
}
|
|
Err(OperationError::RevisionUnavailable)
|
|
}
|
|
|
|
pub fn acknowledge(&mut self, client_id: &str, update_id: u64) {
|
|
self.acknowledged_updates
|
|
.entry(client_id.to_owned())
|
|
.and_modify(|acknowledged| *acknowledged = (*acknowledged).max(update_id))
|
|
.or_insert(update_id);
|
|
}
|
|
|
|
pub fn has_applied_update(&self, client_id: &str, update_id: u64) -> bool {
|
|
self.acknowledged_updates
|
|
.get(client_id)
|
|
.is_some_and(|acknowledged| update_id <= *acknowledged)
|
|
}
|
|
|
|
pub fn acknowledged_updates(&self, client_id: &str) -> Vec<u64> {
|
|
self.acknowledged_updates
|
|
.get(client_id)
|
|
.copied()
|
|
.into_iter()
|
|
.collect()
|
|
}
|
|
|
|
pub fn operations_after(&self, revision_id: i64) -> Option<Vec<AppliedOperation>> {
|
|
if revision_id == self.revision_id {
|
|
return Some(Vec::new());
|
|
}
|
|
let start = self
|
|
.history
|
|
.iter()
|
|
.position(|entry| entry.base_revision_id == revision_id)?;
|
|
let mut expected_revision = revision_id;
|
|
let mut operations = Vec::new();
|
|
for applied in self.history.iter().skip(start) {
|
|
if applied.base_revision_id != expected_revision {
|
|
return None;
|
|
}
|
|
operations.push(applied.clone());
|
|
expected_revision = applied.revision_id;
|
|
if expected_revision == self.revision_id {
|
|
return Some(operations);
|
|
}
|
|
}
|
|
None
|
|
}
|
|
|
|
pub fn record(&mut self, operation: AppliedOperation) {
|
|
self.acknowledge(&operation.client_id, operation.update_id);
|
|
self.history_bytes = self
|
|
.history_bytes
|
|
.saturating_add(applied_operation_size(&operation));
|
|
self.history.push_back(operation);
|
|
while self.history.len() > MAX_OPERATION_HISTORY
|
|
|| self.history_bytes > MAX_OPERATION_HISTORY_BYTES
|
|
{
|
|
let Some(removed) = self.history.pop_front() else {
|
|
break;
|
|
};
|
|
self.history_bytes = self
|
|
.history_bytes
|
|
.saturating_sub(applied_operation_size(&removed));
|
|
}
|
|
}
|
|
}
|
|
|
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
|
enum ComponentKind {
|
|
Retain,
|
|
Delete,
|
|
Insert,
|
|
}
|
|
|
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
|
pub enum OperationError {
|
|
InvalidComponent,
|
|
InvalidUtf16Boundary,
|
|
LengthMismatch,
|
|
TooManyComponents,
|
|
RevisionUnavailable,
|
|
Serialization,
|
|
}
|
|
|
|
impl fmt::Display for OperationError {
|
|
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
|
|
let message = match self {
|
|
Self::InvalidComponent => "invalid text operation component",
|
|
Self::InvalidUtf16Boundary => "text operation splits a UTF-16 character",
|
|
Self::LengthMismatch => "text operation length does not match the document",
|
|
Self::TooManyComponents => "text operation contains too many components",
|
|
Self::RevisionUnavailable => "the base revision is no longer available",
|
|
Self::Serialization => "invalid authorship metadata",
|
|
};
|
|
formatter.write_str(message)
|
|
}
|
|
}
|
|
|
|
impl Error for OperationError {}
|
|
|
|
fn applied_operation_size(operation: &AppliedOperation) -> usize {
|
|
let components = operation
|
|
.operation
|
|
.components
|
|
.iter()
|
|
.map(|component| match component {
|
|
OperationComponent::Retain { .. } | OperationComponent::Delete { .. } => 24,
|
|
OperationComponent::Insert { text, owners } => {
|
|
32usize.saturating_add(text.len()).saturating_add(
|
|
owners
|
|
.iter()
|
|
.map(|span| 24usize.saturating_add(span.owner.len()))
|
|
.sum::<usize>(),
|
|
)
|
|
}
|
|
})
|
|
.sum::<usize>();
|
|
components
|
|
.saturating_add(operation.client_id.len())
|
|
.saturating_add(
|
|
operation
|
|
.owner_replacements
|
|
.iter()
|
|
.map(|replacement| replacement.owner.len() + replacement.replacement.len() + 16)
|
|
.sum::<usize>(),
|
|
)
|
|
}
|
|
|
|
fn operation_key_before(
|
|
left_client_id: &str,
|
|
left_update_id: u64,
|
|
right_client_id: &str,
|
|
right_update_id: u64,
|
|
) -> bool {
|
|
left_client_id < right_client_id
|
|
|| (left_client_id == right_client_id && left_update_id < right_update_id)
|
|
}
|
|
|
|
fn component_kind(component: &OperationComponent) -> ComponentKind {
|
|
match component {
|
|
OperationComponent::Retain { .. } => ComponentKind::Retain,
|
|
OperationComponent::Delete { .. } => ComponentKind::Delete,
|
|
OperationComponent::Insert { .. } => ComponentKind::Insert,
|
|
}
|
|
}
|
|
|
|
fn component_length(component: &OperationComponent) -> usize {
|
|
match component {
|
|
OperationComponent::Retain { count } | OperationComponent::Delete { count } => *count,
|
|
OperationComponent::Insert { text, .. } => text.encode_utf16().count(),
|
|
}
|
|
}
|
|
|
|
fn normalize_owner_spans(spans: &[OwnerSpan], length: usize) -> Vec<OwnerSpan> {
|
|
let mut sorted = spans
|
|
.iter()
|
|
.filter_map(|span| {
|
|
let start = span.start.min(length);
|
|
let end = span.end.min(length).max(start);
|
|
if span.owner.is_empty() || end <= start {
|
|
None
|
|
} else {
|
|
Some(OwnerSpan {
|
|
start,
|
|
end,
|
|
owner: span.owner.clone(),
|
|
})
|
|
}
|
|
})
|
|
.collect::<Vec<_>>();
|
|
sorted.sort_by_key(|span| (span.start, span.end));
|
|
let mut result: Vec<OwnerSpan> = Vec::new();
|
|
for mut span in sorted {
|
|
if let Some(previous) = result.last_mut() {
|
|
if previous.owner == span.owner && span.start <= previous.end {
|
|
previous.end = previous.end.max(span.end);
|
|
continue;
|
|
}
|
|
if span.start < previous.end {
|
|
span.start = previous.end;
|
|
}
|
|
}
|
|
if span.end > span.start {
|
|
result.push(span);
|
|
}
|
|
}
|
|
result
|
|
}
|
|
|
|
fn slice_owner_spans(spans: &[OwnerSpan], start: usize, length: usize) -> Vec<OwnerSpan> {
|
|
let end = start.saturating_add(length);
|
|
let sliced = spans
|
|
.iter()
|
|
.filter_map(|span| {
|
|
let overlap_start = start.max(span.start);
|
|
let overlap_end = end.min(span.end);
|
|
(overlap_end > overlap_start).then(|| OwnerSpan {
|
|
start: overlap_start - start,
|
|
end: overlap_end - start,
|
|
owner: span.owner.clone(),
|
|
})
|
|
})
|
|
.collect::<Vec<_>>();
|
|
normalize_owner_spans(&sliced, length)
|
|
}
|
|
|
|
fn shift_owner_spans(spans: &[OwnerSpan], offset: usize) -> Vec<OwnerSpan> {
|
|
spans
|
|
.iter()
|
|
.map(|span| OwnerSpan {
|
|
start: span.start + offset,
|
|
end: span.end + offset,
|
|
owner: span.owner.clone(),
|
|
})
|
|
.collect()
|
|
}
|
|
|
|
fn append_component(
|
|
components: &mut Vec<OperationComponent>,
|
|
component: OperationComponent,
|
|
) -> Result<(), OperationError> {
|
|
match component {
|
|
OperationComponent::Retain { count } => {
|
|
if count == 0 {
|
|
return Ok(());
|
|
}
|
|
if let Some(OperationComponent::Retain { count: previous }) = components.last_mut() {
|
|
*previous = previous
|
|
.checked_add(count)
|
|
.ok_or(OperationError::InvalidComponent)?;
|
|
} else {
|
|
components.push(OperationComponent::Retain { count });
|
|
}
|
|
}
|
|
OperationComponent::Delete { count } => {
|
|
if count == 0 {
|
|
return Ok(());
|
|
}
|
|
if let Some(OperationComponent::Delete { count: previous }) = components.last_mut() {
|
|
*previous = previous
|
|
.checked_add(count)
|
|
.ok_or(OperationError::InvalidComponent)?;
|
|
} else {
|
|
components.push(OperationComponent::Delete { count });
|
|
}
|
|
}
|
|
OperationComponent::Insert { text, owners } => {
|
|
let length = text.encode_utf16().count();
|
|
if length == 0 {
|
|
return Ok(());
|
|
}
|
|
let owners = normalize_owner_spans(&owners, length);
|
|
if let Some(OperationComponent::Insert {
|
|
text: previous_text,
|
|
owners: previous_owners,
|
|
}) = components.last_mut()
|
|
{
|
|
let offset = previous_text.encode_utf16().count();
|
|
previous_text.push_str(&text);
|
|
previous_owners.extend(shift_owner_spans(&owners, offset));
|
|
*previous_owners =
|
|
normalize_owner_spans(previous_owners, previous_text.encode_utf16().count());
|
|
} else {
|
|
components.push(OperationComponent::Insert { text, owners });
|
|
}
|
|
}
|
|
}
|
|
if components.len() > MAX_OPERATION_COMPONENTS {
|
|
return Err(OperationError::TooManyComponents);
|
|
}
|
|
Ok(())
|
|
}
|
|
|
|
pub fn normalize_operation(operation: &TextOperation) -> Result<TextOperation, OperationError> {
|
|
if operation.components.len() > MAX_OPERATION_COMPONENTS {
|
|
return Err(OperationError::TooManyComponents);
|
|
}
|
|
let mut inserted_bytes = 0usize;
|
|
let mut owner_span_count = 0usize;
|
|
let mut components = Vec::with_capacity(operation.components.len());
|
|
for component in &operation.components {
|
|
if let OperationComponent::Insert { text, owners } = component {
|
|
inserted_bytes = inserted_bytes
|
|
.checked_add(text.len())
|
|
.ok_or(OperationError::InvalidComponent)?;
|
|
owner_span_count = owner_span_count
|
|
.checked_add(owners.len())
|
|
.ok_or(OperationError::InvalidComponent)?;
|
|
let text_length = text.encode_utf16().count();
|
|
if inserted_bytes > MAX_OPERATION_INSERT_BYTES
|
|
|| owner_span_count > MAX_OPERATION_OWNER_SPANS
|
|
|| owners.iter().any(|span| {
|
|
span.start > span.end
|
|
|| span.end > text_length
|
|
|| utf16_byte_index(text, span.start).is_err()
|
|
|| utf16_byte_index(text, span.end).is_err()
|
|
|| span.owner.chars().count() > MAX_OWNER_LENGTH
|
|
|| span.owner.chars().any(|character| {
|
|
character.is_control() && character != OWNER_COLOR_SEPARATOR
|
|
})
|
|
})
|
|
{
|
|
return Err(OperationError::InvalidComponent);
|
|
}
|
|
}
|
|
append_component(&mut components, component.clone())?;
|
|
}
|
|
Ok(TextOperation { components })
|
|
}
|
|
|
|
pub fn operation_base_length(operation: &TextOperation) -> Result<usize, OperationError> {
|
|
normalize_operation(operation)?
|
|
.components
|
|
.iter()
|
|
.try_fold(0usize, |length, component| {
|
|
let component_length = match component {
|
|
OperationComponent::Retain { count } | OperationComponent::Delete { count } => {
|
|
*count
|
|
}
|
|
OperationComponent::Insert { .. } => 0,
|
|
};
|
|
length
|
|
.checked_add(component_length)
|
|
.ok_or(OperationError::InvalidComponent)
|
|
})
|
|
}
|
|
|
|
pub fn operation_from_edit(
|
|
previous_content: &str,
|
|
next_content: &str,
|
|
next_owner_map: &str,
|
|
) -> TextOperation {
|
|
let mut previous_prefix_bytes = 0usize;
|
|
let mut next_prefix_bytes = 0usize;
|
|
for (previous, next) in previous_content.chars().zip(next_content.chars()) {
|
|
if previous != next {
|
|
break;
|
|
}
|
|
previous_prefix_bytes += previous.len_utf8();
|
|
next_prefix_bytes += next.len_utf8();
|
|
}
|
|
|
|
let previous_remainder = &previous_content[previous_prefix_bytes..];
|
|
let next_remainder = &next_content[next_prefix_bytes..];
|
|
let mut previous_suffix_bytes = 0usize;
|
|
let mut next_suffix_bytes = 0usize;
|
|
for (previous, next) in previous_remainder
|
|
.chars()
|
|
.rev()
|
|
.zip(next_remainder.chars().rev())
|
|
{
|
|
if previous != next {
|
|
break;
|
|
}
|
|
previous_suffix_bytes += previous.len_utf8();
|
|
next_suffix_bytes += next.len_utf8();
|
|
}
|
|
|
|
let previous_middle_end = previous_content.len() - previous_suffix_bytes;
|
|
let next_middle_end = next_content.len() - next_suffix_bytes;
|
|
let previous_prefix = &previous_content[..previous_prefix_bytes];
|
|
let previous_middle = &previous_content[previous_prefix_bytes..previous_middle_end];
|
|
let next_middle = &next_content[next_prefix_bytes..next_middle_end];
|
|
let suffix = &previous_content[previous_middle_end..];
|
|
|
|
let prefix_length = previous_prefix.encode_utf16().count();
|
|
let deleted_length = previous_middle.encode_utf16().count();
|
|
let inserted_length = next_middle.encode_utf16().count();
|
|
let suffix_length = suffix.encode_utf16().count();
|
|
let next_authorship = parse_authorship(next_content, next_owner_map);
|
|
let inserted_owners = slice_owner_spans(&next_authorship.spans, prefix_length, inserted_length);
|
|
|
|
let mut components = Vec::new();
|
|
if prefix_length > 0 {
|
|
components.push(OperationComponent::Retain {
|
|
count: prefix_length,
|
|
});
|
|
}
|
|
if deleted_length > 0 {
|
|
components.push(OperationComponent::Delete {
|
|
count: deleted_length,
|
|
});
|
|
}
|
|
if !next_middle.is_empty() {
|
|
components.push(OperationComponent::Insert {
|
|
text: next_middle.to_owned(),
|
|
owners: inserted_owners,
|
|
});
|
|
}
|
|
if suffix_length > 0 {
|
|
components.push(OperationComponent::Retain {
|
|
count: suffix_length,
|
|
});
|
|
}
|
|
TextOperation { components }
|
|
}
|
|
|
|
pub fn replace_operation(
|
|
base_length: usize,
|
|
content: String,
|
|
owners: Vec<OwnerSpan>,
|
|
) -> TextOperation {
|
|
let mut components = Vec::new();
|
|
if base_length > 0 {
|
|
components.push(OperationComponent::Delete { count: base_length });
|
|
}
|
|
if !content.is_empty() {
|
|
components.push(OperationComponent::Insert {
|
|
text: content,
|
|
owners,
|
|
});
|
|
}
|
|
TextOperation { components }
|
|
}
|
|
|
|
struct OperationCursor {
|
|
components: Vec<OperationComponent>,
|
|
index: usize,
|
|
offset: usize,
|
|
}
|
|
|
|
impl OperationCursor {
|
|
fn new(operation: &TextOperation) -> Result<Self, OperationError> {
|
|
Ok(Self {
|
|
components: normalize_operation(operation)?.components,
|
|
index: 0,
|
|
offset: 0,
|
|
})
|
|
}
|
|
|
|
fn current(&self) -> Option<&OperationComponent> {
|
|
self.components.get(self.index)
|
|
}
|
|
|
|
fn kind(&self) -> Option<ComponentKind> {
|
|
self.current().map(component_kind)
|
|
}
|
|
|
|
fn remaining(&self) -> usize {
|
|
self.current()
|
|
.map(|component| component_length(component).saturating_sub(self.offset))
|
|
.unwrap_or(0)
|
|
}
|
|
|
|
fn take(&mut self, count: usize) -> Result<OperationComponent, OperationError> {
|
|
let component = self
|
|
.current()
|
|
.cloned()
|
|
.ok_or(OperationError::InvalidComponent)?;
|
|
if count == 0 || count > self.remaining() {
|
|
return Err(OperationError::InvalidComponent);
|
|
}
|
|
let component_length = component_length(&component);
|
|
let part = match component {
|
|
OperationComponent::Retain { .. } => OperationComponent::Retain { count },
|
|
OperationComponent::Delete { .. } => OperationComponent::Delete { count },
|
|
OperationComponent::Insert { text, owners } => OperationComponent::Insert {
|
|
text: slice_utf16(&text, self.offset, count)?.to_owned(),
|
|
owners: slice_owner_spans(&owners, self.offset, count),
|
|
},
|
|
};
|
|
self.offset += count;
|
|
if self.offset == component_length {
|
|
self.index += 1;
|
|
self.offset = 0;
|
|
}
|
|
Ok(part)
|
|
}
|
|
|
|
fn take_remaining(&mut self) -> Result<OperationComponent, OperationError> {
|
|
let count = self.remaining();
|
|
self.take(count)
|
|
}
|
|
}
|
|
|
|
pub fn transform_operation(
|
|
left_operation: &TextOperation,
|
|
right_operation: &TextOperation,
|
|
left_before_right: bool,
|
|
) -> Result<TextOperation, OperationError> {
|
|
if operation_base_length(left_operation)? != operation_base_length(right_operation)? {
|
|
return Err(OperationError::LengthMismatch);
|
|
}
|
|
let mut left = OperationCursor::new(left_operation)?;
|
|
let mut right = OperationCursor::new(right_operation)?;
|
|
let mut left_prime = Vec::new();
|
|
|
|
while left.current().is_some() || right.current().is_some() {
|
|
if left.kind() == Some(ComponentKind::Insert)
|
|
&& (right.kind() != Some(ComponentKind::Insert) || left_before_right)
|
|
{
|
|
append_component(&mut left_prime, left.take_remaining()?)?;
|
|
continue;
|
|
}
|
|
if right.kind() == Some(ComponentKind::Insert) {
|
|
let count = right.remaining();
|
|
right.take_remaining()?;
|
|
append_component(&mut left_prime, OperationComponent::Retain { count })?;
|
|
continue;
|
|
}
|
|
let (Some(left_kind), Some(right_kind)) = (left.kind(), right.kind()) else {
|
|
return Err(OperationError::InvalidComponent);
|
|
};
|
|
let count = left.remaining().min(right.remaining());
|
|
match (left_kind, right_kind) {
|
|
(ComponentKind::Retain, ComponentKind::Retain) => {
|
|
append_component(&mut left_prime, OperationComponent::Retain { count })?;
|
|
}
|
|
(ComponentKind::Delete, ComponentKind::Retain) => {
|
|
append_component(&mut left_prime, OperationComponent::Delete { count })?;
|
|
}
|
|
(ComponentKind::Retain, ComponentKind::Delete)
|
|
| (ComponentKind::Delete, ComponentKind::Delete) => {}
|
|
_ => return Err(OperationError::InvalidComponent),
|
|
}
|
|
left.take(count)?;
|
|
right.take(count)?;
|
|
}
|
|
Ok(TextOperation {
|
|
components: left_prime,
|
|
})
|
|
}
|
|
|
|
fn utf16_byte_index(value: &str, offset: usize) -> Result<usize, OperationError> {
|
|
if offset == 0 {
|
|
return Ok(0);
|
|
}
|
|
let mut current = 0usize;
|
|
for (byte_index, character) in value.char_indices() {
|
|
if current == offset {
|
|
return Ok(byte_index);
|
|
}
|
|
current += character.len_utf16();
|
|
if current > offset {
|
|
return Err(OperationError::InvalidUtf16Boundary);
|
|
}
|
|
}
|
|
if current == offset {
|
|
Ok(value.len())
|
|
} else {
|
|
Err(OperationError::LengthMismatch)
|
|
}
|
|
}
|
|
|
|
fn slice_utf16(value: &str, start: usize, length: usize) -> Result<&str, OperationError> {
|
|
let start_byte = utf16_byte_index(value, start)?;
|
|
let end_byte = utf16_byte_index(value, start.saturating_add(length))?;
|
|
value
|
|
.get(start_byte..end_byte)
|
|
.ok_or(OperationError::InvalidUtf16Boundary)
|
|
}
|
|
|
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
struct AuthorshipModel {
|
|
#[serde(default = "authorship_version")]
|
|
version: u8,
|
|
#[serde(default)]
|
|
spans: Vec<OwnerSpan>,
|
|
}
|
|
|
|
fn authorship_version() -> u8 {
|
|
AUTHORSHIP_VERSION
|
|
}
|
|
|
|
#[derive(Debug, Deserialize)]
|
|
#[serde(untagged)]
|
|
enum RawAuthorship {
|
|
Model(AuthorshipModel),
|
|
LineOwners(Vec<String>),
|
|
}
|
|
|
|
fn parse_authorship(content: &str, raw: &str) -> AuthorshipModel {
|
|
let length = content.encode_utf16().count();
|
|
match serde_json::from_str::<RawAuthorship>(raw) {
|
|
Ok(RawAuthorship::Model(model)) if model.version == AUTHORSHIP_VERSION => AuthorshipModel {
|
|
version: AUTHORSHIP_VERSION,
|
|
spans: normalize_owner_spans(&model.spans, length),
|
|
},
|
|
Ok(RawAuthorship::LineOwners(owners)) => {
|
|
let lines = content.split('\n').collect::<Vec<_>>();
|
|
let mut offset = 0usize;
|
|
let mut spans = Vec::new();
|
|
for (index, line) in lines.iter().enumerate() {
|
|
let line_length =
|
|
line.encode_utf16().count() + usize::from(index + 1 < lines.len());
|
|
let owner = owners.get(index).cloned().unwrap_or_default();
|
|
if !owner.is_empty() && line_length > 0 {
|
|
spans.push(OwnerSpan {
|
|
start: offset,
|
|
end: offset + line_length,
|
|
owner,
|
|
});
|
|
}
|
|
offset += line_length;
|
|
}
|
|
AuthorshipModel {
|
|
version: AUTHORSHIP_VERSION,
|
|
spans: normalize_owner_spans(&spans, length),
|
|
}
|
|
}
|
|
_ => AuthorshipModel {
|
|
version: AUTHORSHIP_VERSION,
|
|
spans: Vec::new(),
|
|
},
|
|
}
|
|
}
|
|
|
|
fn copy_retained_spans(
|
|
target: &mut Vec<OwnerSpan>,
|
|
spans: &[OwnerSpan],
|
|
source_start: usize,
|
|
length: usize,
|
|
output_start: usize,
|
|
) {
|
|
let source_end = source_start + length;
|
|
for span in spans {
|
|
let start = source_start.max(span.start);
|
|
let end = source_end.min(span.end);
|
|
if end > start {
|
|
target.push(OwnerSpan {
|
|
start: output_start + start - source_start,
|
|
end: output_start + end - source_start,
|
|
owner: span.owner.clone(),
|
|
});
|
|
}
|
|
}
|
|
}
|
|
|
|
pub fn apply_operation_to_document(
|
|
content: &str,
|
|
owner_map: &str,
|
|
operation: &TextOperation,
|
|
owner_replacements: &[OwnerReplacement],
|
|
) -> Result<(String, String), OperationError> {
|
|
let operation = normalize_operation(operation)?;
|
|
let content_length = content.encode_utf16().count();
|
|
if operation_base_length(&operation)? != content_length {
|
|
return Err(OperationError::LengthMismatch);
|
|
}
|
|
|
|
let source_model = parse_authorship(content, owner_map);
|
|
let mut output_spans = Vec::new();
|
|
let mut source_offset = 0usize;
|
|
let mut output_offset = 0usize;
|
|
let mut output_content = String::new();
|
|
|
|
for component in &operation.components {
|
|
match component {
|
|
OperationComponent::Retain { count } => {
|
|
output_content.push_str(slice_utf16(content, source_offset, *count)?);
|
|
copy_retained_spans(
|
|
&mut output_spans,
|
|
&source_model.spans,
|
|
source_offset,
|
|
*count,
|
|
output_offset,
|
|
);
|
|
source_offset += *count;
|
|
output_offset += *count;
|
|
}
|
|
OperationComponent::Delete { count } => {
|
|
source_offset += *count;
|
|
}
|
|
OperationComponent::Insert { text, owners } => {
|
|
output_content.push_str(text);
|
|
output_spans.extend(shift_owner_spans(owners, output_offset));
|
|
output_offset += text.encode_utf16().count();
|
|
}
|
|
}
|
|
}
|
|
if source_offset != content_length {
|
|
return Err(OperationError::LengthMismatch);
|
|
}
|
|
|
|
for span in &mut output_spans {
|
|
let identity = span
|
|
.owner
|
|
.split(OWNER_COLOR_SEPARATOR)
|
|
.next()
|
|
.unwrap_or_default();
|
|
if let Some(replacement) = owner_replacements.iter().find(|replacement| {
|
|
replacement.owner == identity && !replacement.replacement.is_empty()
|
|
}) {
|
|
span.owner.clone_from(&replacement.replacement);
|
|
}
|
|
}
|
|
let model = AuthorshipModel {
|
|
version: AUTHORSHIP_VERSION,
|
|
spans: normalize_owner_spans(&output_spans, output_offset),
|
|
};
|
|
let owner_map = serde_json::to_string(&model).map_err(|_| OperationError::Serialization)?;
|
|
Ok((output_content, owner_map))
|
|
}
|
|
|
|
pub fn owner_spans_from_map(content: &str, owner_map: &str) -> Vec<OwnerSpan> {
|
|
parse_authorship(content, owner_map).spans
|
|
}
|
|
|
|
#[cfg(test)]
|
|
#[path = "tests/collab.rs"]
|
|
mod tests;
|